skip to content

In PHP, which Fiber misuses throw FiberError, and what happens to an exception thrown inside a fiber or injected with Fiber::throw()?

level: middleimportance: should knowfreq 18%

answer

  1. FiberError extends Error, not Exception
  2. start twice, resume when not suspended
  3. suspend outside a fiber
  4. getReturn before a normal return
  5. throw() raises at the suspend point

basics

~10 s

FiberError, an Error subclass, marks wrong-state calls: starting twice, resuming a non-suspended fiber, Fiber::suspend() outside a fiber, an early getReturn(). Exceptions inside a fiber leave through the start(), resume() or throw() that entered it.

solid answer

~40 s

`FiberError` extends `Error`, so `catch (Exception $e)` does not catch it. The engine throws it for state misuse: `start()` on a fiber already started, `resume()` or `throw()` on one that is not suspended (not started, running or terminated), `Fiber::suspend()` outside any fiber, `getReturn()` when the fiber has not started, has not returned yet or threw, and any switch in a context where switching is blocked, such as a pcntl signal handler. Exceptions follow the switch: an uncaught throwable inside the fiber comes out of whichever `start()`, `resume()` or `throw()` call switched into it, and the fiber is terminated. `$fiber->throw($e)` resumes the fiber by throwing `$e` from its paused `Fiber::suspend()` call, where it can be caught. Since PHP 8.4 fibers may also switch during destructors.

code

php · 21 lines
php
<?php
declare(strict_types=1);

$fiber = new Fiber(function (): string {
    try {
        Fiber::suspend('waiting');
    } catch (RuntimeException $e) {
        return 'handled: ' . $e->getMessage();
    }
    return 'resumed normally';
});

$fiber->start();
$fiber->throw(new RuntimeException('read timed out'));
echo $fiber->getReturn(), "\n";   // handled: read timed out

try {
    $fiber->resume();              // already terminated
} catch (FiberError $e) {
    echo $e->getMessage(), "\n";   // Cannot resume a fiber that is not suspended
}

go deeper

for a junior

Recall that FiberError extends Error and is thrown for wrong-state calls, such as resuming a fiber that is not suspended.

for a middle

List the state misuses precisely and trace how an uncaught exception leaves a fiber through the start(), resume() or throw() that entered it.

for a senior

Design cleanup around the unwind: put resource release in finally, deliver I/O failures with throw(), and catch Throwable at the loop boundary.

for a principal

Set a team rule for error propagation across suspensions, so failures surface at a defined boundary instead of being swallowed by a scheduler.

## FiberError is an Error `FiberError` is a `final` class that **extends `Error`**, not `Exception`. That matters in practice: a `catch (Exception $e)` block will not catch it, while `catch (Error $e)` or `catch (Throwable $e)` will. You also cannot create one yourself: its constructor throws an `Error` saying the class is reserved for internal use. It exists to signal one thing: **you asked a fiber to do something its current state does not allow**. ## The misuses that throw FiberError Each case below comes with the engine's message in PHP 8.5: - **Starting twice**: `start()` on a fiber that has already been started — "Cannot start a fiber that has already been started". - **Resuming the wrong state**: `resume()` or `throw()` on a fiber that is not suspended, whether it has not started, is running right now or has terminated — "Cannot resume a fiber that is not suspended". - **Suspending outside a fiber**: `Fiber::suspend()` from the main script — "Cannot suspend outside of a fiber". - **Reading a result too early**: `getReturn()` gives "Cannot get fiber return value:" followed by "The fiber has not been started", "The fiber has not returned" or "The fiber threw an exception". - **Switching where it is blocked**: any switch attempted while the engine forbids switching, for example inside a pcntl signal handler or a tick function — "Cannot switch fibers in current execution context". - **Suspending while being destroyed**: `Fiber::suspend()` in a fiber that the engine is force-closing — "Cannot suspend in a force-closed fiber". ## How exceptions travel A fiber has its own stack, but exceptions do not get lost on it. The rule is: **an exception leaves a fiber through the call that switched into it**. 1. Code inside the fiber throws and nothing inside catches it. 2. The fiber is terminated (`isTerminated()` becomes `true`). 3. The exception is thrown from the `start()`, `resume()` or `throw()` call the caller used to enter the fiber, so the caller can catch it there. 4. `getReturn()` on that fiber now throws `FiberError`, because it threw instead of returning. `isTerminated()` is `true` both for a fiber that returned and for one that threw, so `getReturn()` (or the exception you caught) is how you tell them apart. With nested fibers the rule applies level by level: an inner fiber's exception comes out of the `start()` or `resume()` call made inside the outer fiber, which can catch it or let it continue out to its own caller. ## Injecting an exception with throw() `$fiber->throw(Throwable $e)` is the error-path twin of `resume()`. It continues a suspended fiber, but instead of making `Fiber::suspend()` return a value, it makes that call **throw `$e`**. Code inside the fiber can wrap the suspension in `try`/`catch` and handle it, for example to turn a timed-out socket read into a domain exception. If the fiber does not catch it, the exception comes straight back out of `throw()` on the caller's side. Like `resume()`, `throw()` returns the value of the next suspension, or `null` if the fiber returns. Event-loop libraries use this to deliver I/O failures: when a read fails, the loop throws the error into the waiting fiber, so your code sees an ordinary exception from what looks like a plain function call. ## Fibers that are never finished If a suspended fiber loses its last reference, or the script ends while it is still suspended, the engine **unwinds** it rather than leaking it: | What sits on the fiber's stack | During that unwind | |---|---| | `finally` blocks | they run | | `catch (Throwable $e)` blocks | they do not catch the unwind | | code after the suspension point | it never runs | | a `Fiber::suspend()` inside a `finally` | throws `FiberError` | So cleanup belongs in `finally`, not after the suspension. ## Version notes - **PHP 8.1** introduced `Fiber` and `FiberError`. - **PHP 8.4** allowed fiber switching while an object destructor runs; before 8.4 it was blocked because of conflicts with garbage collection. Destructors that the garbage collector triggers inside a fiber now run in a separate fiber of their own. - The messages above are from php-src 8.5; they are stable but are implementation detail, so code should catch the class, not match the text.

  • Why does catch (Exception $e) around $fiber->resume() miss a FiberError?
    `FiberError` extends `Error`, and `Error` and `Exception` are sibling implementations of `Throwable`. A handler for `Exception` never matches an `Error`. To catch state misuse you catch `FiberError` itself, `Error`, or `Throwable`.
  • What happens to a suspended fiber when the object is unset and nothing else references it?
    The engine resumes it once with an internal unwind so the stack can be torn down: `finally` blocks run, `catch (Throwable $e)` does not intercept the unwind, and nothing after the suspension point executes. Trying to `Fiber::suspend()` again during that unwind throws `FiberError`.

saying these in an interview costs you the question

  • FiberError extends Exception, so catch (Exception) handles it
  • An exception inside a fiber is lost unless the fiber catches it
  • Fiber::throw() throws the exception in the caller, not inside the fiber
  • getReturn() returns null when the fiber has not finished
  • A suspended fiber that is garbage collected just vanishes without running finally