A PHP import script run by cron ends with exit('Import failed') on errors, yet the scheduler records success; why, and how should it report failure?
answer
- a string status prints, then exits 0
- integers 0-254 for your codes
- 255 means a fatal error
- exit() is a function since PHP 8.4
- finally blocks do not run on exit
basics
~20 sexit() with a string prints it and ends with status 0, meaning success. Write the message to STDERR and exit() with an integer: 0 for success, 1-254 for failures; PHP itself uses 255 for fatal errors and uncaught exceptions.
solid answer
~40 s`exit(string|int $status = 0): never` has two modes. An `int` becomes the process exit status; a `string` is printed to standard output and the status is `0`. So `exit('Import failed')` tells cron, CI and `&&` chains that everything worked. The fix is `fwrite(STDERR, "Import failed\n"); exit(1);`. Keep your codes in 0-254: PHP itself ends with 255 on a fatal error or an uncaught exception, so 255 means "PHP crashed", and the operating system keeps only the low eight bits. Since PHP 8.4 `exit()` behaves like a function, so under `strict_types` a wrong type throws a `TypeError`. `exit()` runs shutdown functions and destructors but not `finally` blocks, so cleanup that must happen belongs in those.
code
php · 20 lines<?php
declare(strict_types=1);
function main(array $argv): int
{
$file = $argv[1] ?? null;
if ($file === null) {
fwrite(STDERR, "usage: {$argv[0]} <file.csv>\n");
return 2;
}
try {
$failed = importFile($file); // returns the number of rejected rows
} catch (RuntimeException $e) {
fwrite(STDERR, 'Import failed: ' . $e->getMessage() . "\n");
return 3;
}
return $failed > 0 ? 1 : 0;
}
exit(main($argv)); // one integer exit pointgo deeper
Recall that an integer passed to exit() is the status, 0 means success, and a string passed to exit() is printed while the status stays 0.
Explain which codes PHP produces on its own (255 for fatals and uncaught exceptions), why scripts keep to 0-254, and what the 8.4 exit-as-function change altered.
Diagnose silent cron failures and design commands with one integer exit point, documented codes, messages on STDERR and cleanup that does not depend on finally after exit().
Standardise exit-code meanings across the team's commands so schedulers, CI and alerting can react to them without parsing text output.
## Exit status: the one number the caller sees When a process ends, it hands its parent an **exit status**, a small integer. Shells, cron wrappers, CI runners and process managers read it: - `0` means success; - anything else means failure, and specific values can say which failure. `php import.php && php notify.php` runs the second command only if the first returned `0`. A CI step fails only on a non-zero status. A command that prints an error but returns `0` is, to every tool, a success. ## `exit()` in PHP 8.5 The signature in `Zend/zend_builtin_functions.stub.php` is `exit(string|int $status = 0): never` (`die()` is an alias). It behaves in two ways: | Call | Printed | Exit status | |---|---|---| | `exit;` or `exit()` | nothing | `0` | | `exit(3)` | nothing | `3` | | `exit('Import failed')` | `Import failed` on standard output | `0` | | `exit('1')` | `1` on standard output | `0` — a string, even a numeric one, is printed | That third row is the bug in the question: the message is printed, the status is success, and the scheduler has no reason to alert anyone. The message even goes to standard output, where it can pollute data. ## Reporting failure correctly 1. Write the human message to `STDERR`: `fwrite(STDERR, "Import failed: {$reason}\n");` 2. End with an integer: `exit(1);` 3. Optionally use distinct codes for distinct failures, and document them: - `0` — imported (or, with `--dry-run`, planned) successfully; - `1` — some rows failed validation; - `2` — bad usage, such as an invalid `--limit`; - `3` — the source file could not be read. Keep codes in **0-254**. The manual reserves `255` for PHP, and the operating system keeps only the low eight bits of the status, so values above 255 wrap around (256 becomes 0). ## What PHP does on its own - **Normal end of script:** status `0`, unless an earlier `exit()` set another. - **Fatal error** (`E_ERROR`, parse errors and similar): status `255`. - **Uncaught exception:** reported as a fatal error, so also `255`. - **`php -l` finding a syntax error:** `255`. A well-behaved command catches its expected failures and chooses its own code; a `255` in the logs then reliably means something unexpected crashed. ## What runs when you call `exit()` - Shutdown functions registered with `register_shutdown_function()` run. - Object destructors run. - **`finally` blocks do not run.** `exit()` unwinds the script without executing them, so a `try { ... exit(1); } finally { unlock(); }` never unlocks. Put cleanup that must always happen in a shutdown function or a destructor, or return an exit code from the main function and call `exit()` once, at the very end, outside any `try`. ## The PHP 8.4 change Before PHP 8.4, `exit` was a pure language construct: any non-integer argument was cast to string and printed. Since 8.4 it behaves like a real function: - it follows normal type coercion and respects `declare(strict_types=1)`; - an invalid argument type (an array, an object) throws a `TypeError` instead of being printed; - it can be called with a named argument (`exit(status: 1)`) and used as a callable. It still has its own parser token, so `exit;` without parentheses keeps working. ## A reliable shape for commands - `function main(array $argv): int` returns the code; - expected failures are caught inside `main()` and mapped to codes, with messages on `STDERR`; - the file ends with `exit(main($argv));` — a single, integer exit point.
- Why does a finally block that releases a lock not run after exit(1)?`exit()` terminates the script without executing pending `finally` blocks; only shutdown functions and destructors run. Return the exit code from your main function so the `try`/`finally` completes normally, then call `exit()` once at the top level, or release the lock in a destructor or shutdown function.
- The scheduler reports exit status 255 for the import. What does that suggest?PHP uses 255 when it stops on a fatal error or an uncaught exception, which the manual reserves for PHP itself. If the script's own codes stay within 0-254, a 255 means something unexpected crashed — check the error log rather than the script's own failure paths.
saying these in an interview costs you the question
- exit('Import failed') makes the script return a failure status
- exit('1') exits with status 1
- Exit code 255 is a good choice for a script's own errors
- finally blocks always run, even after exit()
- An uncaught exception ends a CLI script with status 1