What does calling `gen.throw(new Error('boom'))` do to a paused JavaScript generator, and what happens if the generator has not started running yet?
answer
- errors can travel inward too
- injected at the suspension point
- the body's catch may handle it
- uncaught means completed and rethrown
- unstarted has entered no try
basics
~20 sthrow() resumes the generator by raising the error at the paused yield, so a try/catch inside the body can handle it and the generator may keep going. If the generator has not started, or has already finished, no body code runs and the error propagates back to the caller.
solid answer
~40 s`gen.throw(err)` is a third way to resume a generator: instead of delivering a value to the paused `yield`, it makes that `yield` expression throw `err` inside the body. If the `yield` sits inside a `try`/`catch`, the body catches it and carries on; if it then reaches another `yield`, `throw()` returns `{ value, done: false }` like a normal resume. If nothing in the body catches it, the error unwinds the generator — running any `finally` blocks — the generator becomes completed, and the error is rethrown out of the `throw()` call to the caller. The special case is a generator suspended before its first statement: no `try` has been entered, so the body never runs at all, the generator is completed, and the error simply propagates back. The same happens on an already-completed generator.
code
javascript · 14 linesfunction* worker() {
while (true) {
try {
const task = yield 'ready';
console.log('did', task);
} catch (err) {
console.log('recovered from', err.message);
}
}
}
const g = worker();
g.next(); // { value: 'ready', done: false }
console.log(g.throw(new Error('boom')).value); // 'recovered from boom' then 'ready'go deeper
Know that a generator object has throw() alongside next(), and that it is a way for the caller to signal a failure into the generator rather than just read values out.
Explain that throw(err) makes the paused yield throw inside the body: a surrounding try/catch can handle it and the generator resumes with done: false, while an uncaught error completes the generator and is rethrown to the caller.
Demonstrate the edge cases you would guard in a driver — wrap throw() in your own try/catch, remember finally still runs during unwinding, and know that an unstarted or finished generator never enters its own handler.
Frame it as an error-delivery contract between driver and body: decide whether failures should be reported at the suspension point so the producer owns retry policy, or kept outside so the generator stays a pure producer.
## Three ways to resume A suspended generator can be resumed with three different completions. `next(v)` delivers a normal value, so the paused `yield` evaluates to `v`. `return(v)` delivers a return completion, so the paused `yield` behaves like a `return v`. `throw(err)` delivers a throw completion: the paused `yield` expression **throws** `err` inside the generator body, at exactly the point where it was suspended. That symmetry is the mental model to carry into the interview: `yield` is a two-way door, and errors travel through it inward just as values do. ## Catching it inside the body ```javascript function* worker() { while (true) { try { const task = yield 'ready'; console.log('did', task); } catch (err) { console.log('recovered from', err.message); } } } const g = worker(); g.next(); // { value: 'ready', done: false } console.log(g.throw(new Error('boom')).value); // logs 'recovered from boom', then 'ready' ``` The generator was paused at `yield 'ready'` inside a `try`. `throw()` makes that expression throw; the `catch` handles it; the `while` loop goes round again and hits `yield 'ready'` a second time. Because the generator suspended again rather than finishing, `throw()` returns a perfectly ordinary `{ value: 'ready', done: false }` result. From the caller's point of view, `throw()` behaved like `next()` that happened to deliver bad news. ## When nothing catches it If the body has no handler for the error, it unwinds. Enclosing `finally` blocks still run — this is ordinary exception propagation inside the generator's own context — the generator is marked completed, and the error is rethrown out of the `gen.throw(...)` call itself. So the caller must be prepared for `throw()` to throw: ```javascript function* plain() { yield 1; yield 2; } const g = plain(); g.next(); try { g.throw(new Error('boom')); // rethrown here — nothing in the body catches } catch (err) { console.log('caller saw', err.message); // 'caller saw boom' } console.log(g.next()); // { value: undefined, done: true } ``` Notice the generator is finished afterwards. An uncaught injected error is a completion, not a hiccup. ## The not-yet-started case The case people get wrong is a generator that has never been advanced. It is suspended *before* the first statement, which means it has entered no `try` block — there is literally no handler in scope, because none of the body has executed. ```javascript function* g2() { try { yield 1; } catch (e) { console.log('caught'); // never logs in this scenario } } const g = g2(); try { g.throw(new Error('early')); } catch (e) { console.log('propagated', e.message); // 'propagated early' } ``` No body code runs, `'caught'` is never logged, the generator becomes completed, and the error goes straight back to the caller. The same holds for a generator that has already completed: `throw()` on it rethrows to the caller rather than doing anything internally. Compare this with `return()` on an unstarted generator, which quietly completes it with `{ value, done: true }` instead of throwing. ## What it is actually for Most application code never calls `throw()`. Its real users are drivers — code that pulls values from a generator, does something fallible with each one, and wants the failure reported *at the point in the generator that produced it* so the generator can decide whether to retry, skip, or give up. Because the body sees a normal exception at the `yield`, it can express that policy in plain `try`/`catch`, which is far more readable than threading error objects through `next()` and checking them by hand. This same shape — deliver a rejection into the body so `try`/`catch` around the suspension point handles it — is what makes coroutine-style error handling work in general. ## Interview traps The things that separate answers: knowing that a caught injection produces an ordinary `done: false` result rather than ending the generator; knowing that an uncaught one both completes the generator and rethrows to the caller; knowing that `finally` still runs during that unwinding; and knowing the unstarted generator ignores its own `catch` because it never entered the `try`.
- If the generator catches the injected error and yields again, what does throw() return?An ordinary result object with `done: false` and the newly yielded value — indistinguishable from what `next()` would have returned. A caught injection is just a resume that happened to deliver an exception, so the generator remains live and the caller can keep driving it.
- Do finally blocks run when an injected error is not caught?Yes. The error unwinds the generator's own execution context exactly like an ordinary throw, so every `finally` whose `try` had been entered runs on the way out. Only then is the generator marked completed and the error rethrown from the `throw()` call to the caller.
- How does throw() differ from return() on a generator that has not started?Neither runs any body code, because no `try` has been entered. `return(v)` quietly completes the generator and gives back `{ value: v, done: true }`, while `throw(err)` completes it and rethrows `err` to the caller, so the caller needs its own `try`/`catch`.
saying these in an interview costs you the question
- Thinks throw() always propagates without entering the body
- Believes the body's catch runs even before the first next()
- Expects throw() to skip finally blocks during unwinding
- Assumes a caught injection still ends the generator
- Confuses throw() with the generator throwing on its own