Click the Castor logo or press Ctrl Alt T to change theme.
# Error handling Castor Process keeps process execution errors and stream IO errors separate. This is true whether you use the one-shot `Process::run()` helper or the lower-level `Process::start()` lifecycle API. ## Non-zero exit codes are not exceptions A child command can fail in its own domain and still be represented successfully by a `Process` object. For example, a compiler can return `1`, a test runner can return `2`, or a script can call `exit(7)`. ```php <?php use Castor\Os\Process; use function Castor\Io\read_all; $process = Process::start(['php', '-r', 'fwrite(STDERR, "failed\\n"); exit(7);']); $code = $process->wait(); $stderr = read_all($process->stderr); $process->close(); if ($code !== 0) { throw new RuntimeException("Command exited with {$code}: {$stderr}"); } ``` Castor Process returns the exit code and leaves policy to your application. With `Process::run()`, the same command can be written more directly: ```php $result = Process::run(['php', '-r', 'fwrite(STDERR, "failed\\n"); exit(7);']); if ($result->failed()) { throw new RuntimeException("Command exited with {$result->exitCode}: {$result->stderr}"); } ``` ## Startup and proc_open failures `Process::start()` delegates to PHP's `proc_open()`. Startup can fail because of invalid arguments, disabled functions, descriptor problems, missing permissions, platform limitations, or operating-system errors. Some failures are reported by PHP while opening the process. Other failures are command-level results: the shell or executable starts successfully, prints an error to `stderr`, and exits with a non-zero code. Handle both layers: ```php try { $process = Process::start(['some-command']); } catch (Throwable $error) { // PHP could not open the process or attach its pipes. throw new RuntimeException('Unable to start process', previous: $error); } $code = $process->wait(); $stderr = read_all($process->stderr); $process->close(); if ($code !== 0) { // The process started, but the command reported failure. } ``` ## EOF is part of normal reading Castor IO readers signal end-of-file with `Castor\Io\EndOfFile`. Helpers such as `read_all()` handle EOF internally and return the bytes read before EOF. When reading manually, catch EOF to stop your loop: ```php use Castor\Io\EndOfFile; while (true) { try { $process->stdout->read(4096, $chunk); echo $chunk; } catch (EndOfFile) { break; } } ``` EOF on `stdout` or `stderr` means that pipe has no more bytes. It does not by itself mean the process succeeded; inspect the exit code separately. ## Timeouts A timeout is different from a non-zero exit code. Non-zero exits are normal command results, but a timeout means Castor Process stopped waiting, terminated the direct child process, and threw `Castor\Os\Process\TimeoutException`. The exception carries the `Process` instance so you can read any output the child produced before it was terminated: ```php <?php use Castor\Os\Process; use Castor\Os\Process\TimeoutException; try { Process::run(['php', '-r', 'echo "partial"; sleep(10);'], timeout: 1.0); } catch (TimeoutException $error) { $stdout = $error->process->stdout->collect(); // partial $error->process->close(); } ``` Always close the process after reading the streams you need. `Process::run()` closes resources for completed commands, but it leaves the timed-out process open so the exception handler can inspect stdout and stderr. ## IO errors Castor IO operations can throw `Castor\Io\Error` for stream-level failures. Examples include reading or writing a closed stream, broken pipes, invalid resources, or lower-level PHP stream errors. ```php use Castor\Io\Error; try { $process->stdin->write('payload'); $process->stdin->sendLine('answer'); } catch (Error $error) { // The stdin pipe could not be written. } ``` If you call `Process::close()` and then write to `stdin`, the write can fail with `Castor\Io\Error` because the stream is already closed. ## Signals Signal termination is not represented as a normal exit code. If a process is killed or terminated by signal, `getExitCode()` returns `-1`. Use `isSignaled()` and `getTermSignal()` to inspect signal details. ```php $process->terminate(); while ($process->isRunning()) { usleep(10_000); } if ($process->isSignaled()) { $signal = $process->getTermSignal(); } ``` ## Cleanup in finally blocks Use `finally` when your code can throw while reading, writing, or interpreting the process result. ```php $process = Process::start(['php', '-r', 'echo "ok";']); try { $code = $process->wait(); $stdout = read_all($process->stdout); } finally { $process->close(); } ``` `close()` is idempotent: after the first successful close, later calls return immediately. Explicit cleanup prevents open pipes from lingering until object destruction. ## Avoid hidden hangs Two common protocol mistakes look like errors but are really waiting problems: - The parent writes input but never closes `stdin`, while the child waits for EOF. - The parent calls `wait()` while the child writes enough data to fill `stdout` or `stderr`, so the child blocks before it can exit. For commands that read stdin, close `stdin` after the last write. For commands that can produce large output, drain the output streams while the process runs.
Castor ecosystem