0

Fibers deep dive — Fiber::start, suspend, resume, getReturn

Advanced5 min read·php-11-002
interviewcompare

Concept

A Fiber in PHP 8.1 is a primitive for cooperative multitasking. Think of it as a function that can pause itself mid-execution, hand control back to the caller, and later be resumed from the exact point it paused — with its entire call stack and local variable state preserved. This is fundamentally different from calling a function and waiting for it to return.

The Fiber lifecycle has four states: created (instantiated but not started), running (currently executing), suspended (paused at a Fiber::suspend() call), and terminated (returned or threw). The state machine is strict — you cannot resume a terminated Fiber, and calling start() on an already-started Fiber throws a FiberError.

Fiber::suspend(mixed $value = null) is a static method called from within the Fiber's body. It immediately pauses execution and returns the suspended value to whoever called $fiber->start() or $fiber->resume(). This channel enables bidirectional communication: values flow from Fiber to caller via suspend(), and values flow from caller into Fiber via the argument passed to $fiber->resume($value), which becomes the return value of the Fiber::suspend() call inside the Fiber.

$fiber->getReturn() retrieves the Fiber's final return value after it has terminated. Calling it before termination throws a FiberError. This is how a Fiber signals its completed result — analogous to a Future resolving.

One critical gotcha: Fiber::suspend() can only be called from within a Fiber's execution context. Calling it from the main thread throws FiberError: Cannot call Fiber::suspend() when not in a fiber. This means libraries that call suspend must be aware they are operating inside a Fiber, which is why event loop libraries like Amp 3 use a global event loop singleton that transparently yields.

Code Example

php
<?php
declare(strict_types=1);

/**
 * Full Fiber API demonstration:
 *   - start() / resume() / suspend() with value passing
 *   - getReturn() after termination
 *   - State inspection methods
 */

$fiber = new \Fiber(function (): string {
    echo "Fiber started\n";

    // Suspend and send a value OUT to the caller.
    // The value passed to resume() comes back as the return value of suspend().
    $receivedFromCaller = \Fiber::suspend('first suspension value');
    echo "Fiber resumed, received from caller: {$receivedFromCaller}\n";

    // Suspend again — no value passed to caller this time.
    \Fiber::suspend();
    echo "Fiber resumed a second time\n";

    return 'final result from fiber';
});

// ----- Caller side -----

// start() runs the Fiber until the first Fiber::suspend() call.
// It returns whatever suspend() was called with.
$suspendedValue = $fiber->start();
echo "Fiber suspended with: {$suspendedValue}\n"; // "first suspension value"
var_dump($fiber->isSuspended()); // bool(true)
var_dump($fiber->isTerminated()); // bool(false)

// resume() continues the Fiber from where it suspended.
// The argument becomes the return value of Fiber::suspend() inside.
$fiber->resume('hello from caller');

// Second resume — Fiber suspends with null this time.
$fiber->resume();

// Fiber has now returned — it is terminated.
var_dump($fiber->isTerminated()); // bool(true)

$return = $fiber->getReturn();
echo "Fiber returned: {$return}\n"; // "final result from fiber"

// -------------------------------------------------------
// Practical pattern: Fiber as a lazy data pipeline
// -------------------------------------------------------

/**
 * A Fiber that yields database rows one at a time to a consumer,
 * simulating a cursor without loading all rows into memory.
 * The consumer signals "done" by passing false to resume().
 */
$rowCursor = new \Fiber(function (): void {
    $fakeRows = [
        ['id' => 1, 'name' => 'Alice'],
        ['id' => 2, 'name' => 'Bob'],
        ['id' => 3, 'name' => 'Carol'],
    ];

    foreach ($fakeRows as $row) {
        $continue = \Fiber::suspend($row);
        if ($continue === false) {
            return; // Consumer signaled early exit.
        }
    }
});

$row = $rowCursor->start();
while ($row !== null && !$rowCursor->isTerminated()) {
    echo "Processing row: {$row['name']}\n";
    $row = $rowCursor->resume(true);
}

Interview Q&A

Q: How does value passing through Fiber::suspend() and resume() work, and what are the type constraints?

Fiber::suspend(mixed $value) sends $value to whoever called start() or resume() — it becomes the return value of that call. Conversely, the argument passed to $fiber->resume(mixed $value) becomes the return value of Fiber::suspend() inside the Fiber. Both accept mixed, so any PHP value can pass through — scalars, objects, arrays, even other Fibers. There are no type constraints at the language level, but well-designed code typically documents the expected types in PHPDoc. The first call to start() can also pass a value: $fiber->start($arg) which is passed as the argument to the Fiber's callable.


Q: What happens if an exception is thrown inside a Fiber?

If an exception escapes the top of the Fiber's callable (is not caught inside the Fiber), it propagates to the caller at the point where start() or resume() was called. The Fiber transitions to terminated state with the exception, and any subsequent call to getReturn() will rethrow that exception. This is the correct error propagation model — the scheduler that resumed the Fiber receives the exception and can handle it like any other thrown exception. This differs from JavaScript Promises where unhandled rejections require explicit .catch() chains.


Q: Why can't Fiber::suspend() be called from the main execution context?

Fiber::suspend() works by saving the current execution stack and returning control to the caller's stack frame. In the main context there is no "caller" stack to return to — PHP's main execution is not itself a Fiber. This is enforced at the engine level: calling Fiber::suspend() outside a Fiber throws FiberError: Cannot call Fiber::suspend() when not in a fiber. This design decision keeps the runtime simple — there is no implicit "main Fiber" to suspend. Libraries like Amp 3 work around this by ensuring all user code runs inside a Fiber created by the event loop driver.