symfony/process · error · LogicException

Pass the callback to the "Process::start" method or call…

Error message

Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".

What it means

waitUntil($callback) polls the running process until the callback returns true, which requires read support on the pipes. If start() was called without a callback and output was never enabled, waitUntil cannot read output for its callback and throws LogicException (stopping the process first), mirroring the wait() restriction.

Solutions

  1. Call $process->enableOutput() before start() (or Process::enableOutputByDefault() globally), then start()/waitUntil().
  2. Pass a callback to start() to implicitly enable read support, then use waitUntil().
  3. If output polling is not needed, replace waitUntil with wait() plus a status check on completion.
  4. Ensure requireProcessIsStarted passes too — waitUntil only works on a started process.

Example fix

// before
$process->start();
$process->waitUntil(fn ($type, $buffer) => str_contains($buffer, 'Ready')); // LogicException

// after
$process->enableOutput();
$process->start();
$process->waitUntil(fn ($type, $buffer) => str_contains($buffer, 'Ready'));
Defensive patterns

Strategy: validation

Validate before calling

$process->enableOutput();
$process->start();
// now safe:
$process->waitUntil(fn (string $type, string $buffer): bool => str_contains($buffer, 'Ready'));

Try / catch

try {
    $process->waitUntil($predicate);
} catch (\LogicException $e) {
    if (str_contains($e->getMessage(), '"Process::waitUntil"')) {
        // restructure: enableOutput() before start, or start($callback)
    }
    throw $e;
}

Prevention

When it happens

Trigger: Calling $process->waitUntil($fn) after start() with no callback and without enableOutput()/enableOutputByDefault(); haveReadSupport() returns false so the exception is raised before any polling.

Common situations: Condition-based waiting (e.g. 'wait until the server prints Ready') on a process started without output collection; migrating from wait() code that had read support to waitUntil() on a start()-without-callback flow.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of symfony/process@99b85026db (2026-09-14). Data as JSON: /api/errors/6db09d7f871efd32. Report an issue: GitHub.

Appendix: source

Thrown at Process.php:524

     * It allows to have feedback from the independent process during execution.
     *
     * @param-immediately-invoked-callable $callback
     *
     * @param (callable('out'|'err', string):bool)|null $callback A PHP callback to run whenever there is some
     *                                                            output available on STDOUT or STDERR
     *
     * @throws RuntimeException         When process timed out
     * @throws LogicException           When process is not yet started
     * @throws ProcessTimedOutException In case the timeout was reached
     */
    public function waitUntil(callable $callback): bool
    {
        $this->requireProcessIsStarted(__FUNCTION__);
        $this->updateStatus(false);

        if (!$this->processPipes->haveReadSupport()) {
            $this->stop(0);
            throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".');
        }
        $callback = $this->buildCallback($callback);

        $ready = false;
        while (true) {
            $this->checkTimeout();
            $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
            $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);

            foreach ($output as $type => $data) {
                if (3 !== $type) {
                    $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready;
                } elseif (!isset($this->fallbackStatus['signaled'])) {
                    $this->fallbackStatus['exitcode'] = (int) $data;
                }
            }
            if ($ready) {
                return true;

View on GitHub (pinned to 99b85026db)