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::wait".

What it means

wait($callback) can only deliver output through a callback if the process pipes have read support — either a callback was given to start() or enableOutput() was called. Otherwise there is nothing to read and calling wait with a callback would hang or lose output, so a LogicException is thrown (after stopping the process) telling you which of the two setup steps is missing.

Solutions

  1. Pass the callback to start(): $process->start($callback); then call wait().
  2. Call $process->enableOutput() (or Process::enableOutputByDefault()) before start, then wait($callback).
  3. If you don't need output, call wait() without a callback and use getOutput() afterwards.
  4. Unify on one style: either run($callback) or start()/wait($callback) with read support enabled.

Example fix

// before
$process->start();
$process->wait(function ($type, $buffer) { echo $buffer; }); // LogicException

// after
$process->start(function ($type, $buffer) { echo $buffer; });
$process->wait();
Defensive patterns

Strategy: validation

Validate before calling

if (!$this->readOutput && null === $callback) {
    // no read support: either enable output or skip callback use
}
$process->enableOutput();
$process->start();
$process->wait($callback);

Try / catch

try {
    $process->wait($callback);
} catch (\LogicException $e) {
    if (str_contains($e->getMessage(), 'Pass the callback to the "Process::start"')) {
        // restart flow: pass callback to start() or enableOutput() first
    }
    throw $e;
}

Prevention

When it happens

Trigger: Calling $process->wait($someCallback) when start() was called without a callback and enableOutput()/enableOutputByDefault() was never invoked; the pipes layer reports haveReadSupport() === false.

Common situations: Mixing the run($callback) style with the start()/wait() style; starting a fire-and-forget process then later deciding to consume output; refactors that moved the callback from start() to wait().

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/225343cf7ae2339f. Report an issue: GitHub.

Appendix: source

Thrown at Process.php:478

     * @param (callable('out'|'err', string):void)|null $callback A PHP callback to run whenever there is some
     *                                                            output available on STDOUT or STDERR
     *
     * @return int The exitcode of the process
     *
     * @throws ProcessTimedOutException When process timed out
     * @throws ProcessSignaledException When process stopped after receiving signal
     * @throws LogicException           When process is not yet started
     */
    public function wait(?callable $callback = null): int
    {
        $this->requireProcessIsStarted(__FUNCTION__);

        $this->updateStatus(false);

        if (null !== $callback) {
            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::wait".');
            }
            $this->callback = $this->buildCallback($callback);
        }

        do {
            $this->checkTimeout();
            $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen());
            $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
        } while ($running);

        while ($this->isRunning()) {
            $this->checkTimeout();
            usleep(1000);
        }

        if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
            throw new ProcessSignaledException($this);
        }

View on GitHub (pinned to 99b85026db)