In PHP, how do Generator::send(), getReturn() and throw() work, and what does send() do to a generator that has not started yet?
answer
- yield is an expression, not only a statement
- send() resumes and returns the next yielded value
- no manual priming needed
- getReturn() before the end throws Exception
- throw() acts as if the yield threw
basics
~20 ssend($v) makes the paused yield evaluate to $v, resumes the body and returns the next yielded value, first running an unstarted generator to its first yield. getReturn() reads the return value after the end; throw() raises an exception at the paused yield.
solid answer
~50 s`yield` is an expression: when the generator is resumed by `next()` or `foreach` it evaluates to `null`, and when resumed by `send($v)` it evaluates to `$v`. `send()` then runs the body to the next `yield` and returns that newly yielded value, or `null` if the generator finishes. If the generator has not started, `send()` first runs it to the first `yield`, so no manual priming call is needed; the first yielded value is simply not returned to the caller. `getReturn()` returns the value of the body's `return` once the generator has finished; calling it earlier throws `Exception` (*Cannot get return value of a generator that hasn't returned*). `throw($e)` behaves as if the paused `yield` were replaced by `throw $e`: the body may catch it and continue, in which case `throw()` returns the next yielded value, or it propagates to the caller.
code
php · 20 lines<?php
declare(strict_types=1);
function statusCounter(): Generator
{
$counts = [];
while (($status = yield) !== null) {
$counts[$status] = ($counts[$status] ?? 0) + 1;
}
return $counts;
}
$counter = statusCounter();
foreach ([200, 404, 200, 503] as $status) {
$counter->send($status); // first call starts the body itself
}
$counter->send(null); // sentinel: the loop ends, the body returns
var_dump($counter->valid()); // bool(false)
print_r($counter->getReturn()); // [200 => 2, 404 => 1, 503 => 1]go deeper
Recall that yield can also receive a value, that send() delivers it, and that getReturn() reads the value of return after the generator ends.
Explain the send() sequence: implicit start to the first yield, delivery, then the return of the next yielded value, and why getReturn() throws while the generator is paused.
Use send(), throw() and getReturn() to build push-style aggregators or task drivers, and trace exactly which value each call returns when reviewing such code.
Judge when bidirectional generators are clearer than an explicit stateful object or a Fiber-based design, given how hard their control flow is to read.
## Two directions through one keyword Most code uses generators in one direction: the generator yields, the consumer reads. The `Generator` class also lets data flow the other way. The key is that `yield` is an **expression** with a result, not only a statement: ```php $received = yield $outgoing; ``` `$outgoing` goes to the consumer. When the generator is resumed, the whole `yield` expression evaluates to whatever the resumer supplied, and that ends up in `$received`. | How the generator is resumed | What the paused `yield` expression evaluates to | |---|---| | `foreach` or `next()` | `null` | | `send($value)` | `$value` | | `throw($exception)` | nothing: `$exception` is thrown at that point | ## send() step by step `Generator::send(mixed $value): mixed` does three things: 1. **Ensure the generator has started.** If the body has not run yet, PHP first runs it to its first `yield`, exactly as `rewind()` or `current()` would. The value yielded there is not returned by this `send()` call. 2. **Deliver and resume.** The paused `yield` expression evaluates to `$value`, and the body runs on. 3. **Return the next yielded value.** When the body reaches its next `yield`, `send()` returns that value. If the body finishes instead, `send()` returns `null`, and calling `send()` on an already finished generator does nothing and returns `null`. The first step is why PHP generators need no priming. Some other languages require the caller to advance a fresh generator once before the first send, because their send cannot deliver to a generator that has not reached a `yield`; PHP's manual notes explicitly that its `send()` does this advance itself. ## getReturn() and the end of a generator Since PHP 7.0 a generator body may `return` a value. That value is not yielded and `foreach` never sees it. The consumer reads it with `getReturn()`: - after the body has finished normally, `getReturn()` returns the `return` value, or `null` if the body ended without one; - while the generator is unstarted or paused, it throws `Exception` with *Cannot get return value of a generator that hasn't returned*; - if the body ended by throwing, there is no return value and the same exception is thrown. This gives a clean split for push-style consumers: values stream in through `send()`, and a summary comes out once through `getReturn()`. ## throw(): injecting a failure `Generator::throw(Throwable $exception): mixed` resumes the generator by raising `$exception` at the paused `yield`: - If the body catches it, execution continues from the `catch` block, and `throw()` returns the next yielded value, just as `send()` would. - If the body does not catch it, the exception propagates out of the generator and out of the `throw()` call to the caller, and the generator is finished. - If the generator is already closed, the exception is thrown directly in the caller's context. This is how a driver tells a paused task that the thing it was waiting for failed. ## Where this is used Bidirectional generators are rarer than plain lazy sequences, but they appear in real PHP code: - **Push-based aggregators.** A generator receives records through `send()`, keeps running totals in its locals, and returns a summary that the caller reads with `getReturn()` after a sentinel ends the loop. - **Coroutine drivers.** Asynchronous libraries written before Fibers used generators as tasks: a task yields a pending operation, and the scheduler resumes it with `send($result)` or `throw($error)`. - **Protocol parsers.** A parser yields "need more bytes" and receives each chunk through `send()`, keeping its parse state across chunks. ## Common mistakes - Expecting the first `send()` to return the first yielded value; it returns the value yielded **after** the sent one is consumed. - Calling `getReturn()` inside a `foreach` over the same generator; it is still running, so the call throws. - Mixing `foreach` and `send()` on one generator, which interleaves `null` and sent values at the same `yield` and makes the logic hard to follow.
- Given function g() { $x = yield 1; echo $x; yield 2; }, what does g()->send('a') echo and return?It echoes `a` and returns `2`. `send()` first runs the unstarted body to `yield 1` (that value is not returned), delivers `'a'` as the result of that `yield`, runs on through the `echo`, and returns the value of the next `yield`.
- Why did asynchronous PHP libraries use send() and throw() before PHP 8.1?A generator can pause mid-function and be resumed with a value or an exception, which is exactly what a task waiting on I/O needs. The scheduler received a pending operation via `yield`, and later resumed the task with `send($result)` or `throw($error)`. Since 8.1, Fibers offer the same suspension without making every function in the chain a generator.
saying these in an interview costs you the question
- An unstarted generator must be primed with next() before the first send().
- send() returns the value of the yield that received the sent value.
- getReturn() returns null while the generator is still running.
- The return value of a generator is yielded as its last foreach element.
- throw() always ends the generator, even when the body catches the exception.