generator.throwError()

I would like there to be a generator.throwError method which would construct and throw an error at the current location where the couroutine (generator) is paused. Without such a method you will simply not be able to tell where a bug in the coroutine code really occurred.

It's a bit long, but here's a real example that shows how all the parts fit together and what's missing:

const buildFacade = (lamp) => ({
  get on() { return lamp.state >= 0b10 }
});

const useLamp = (strategy, lamp) => {
  const co = strategy(buildFacade(lamp));
  let current = co.next();
  let clicks = 0;

  while (!current.done) {
    const action = current.value;
    switch(action) {
      case 'click':
        ++clicks;
        lamp.state = (lamp.state + 0b01) & 0b11;
        break;
      default:
        throw new Error(`unknown {action: ${action}}`);
    }
    current = co.next();
  }

  return clicks;
}

With the setup done we can see some basic correct usages working, as well as the power of the approach:

function* turnOn(lamp) {
  while (!lamp.on) yield 'click';
}

useLamp(turnOn, { state: 0b00 }) === 2;
useLamp(turnOn, { state: 0b01 }) === 1;
useLamp(turnOn, { state: 0b10 }) === 0;
useLamp(turnOn, { state: 0b10 }) === 0;

function* toggle(lamp) {
  const initialState = lamp.on;
  while (lamp.on === initialState) yield 'click';
}

useLamp(toggle, { state: 0b00 }) === 2;
useLamp(toggle, { state: 0b01 }) === 1;
useLamp(toggle, { state: 0b10 }) === 2;
useLamp(toggle, { state: 0b10 }) === 1;

And here's where we get into trouble: this throws an error, but the error is constructed in useLamp and its stack will not point to the yield 'trogdor' line.

function* burninate(lamp) {
  yield 'trogdor';
}

useLamp(burninate, { state: 0b00 });

Also if anyone knows why on earth some lamps work like this I'd like to know that too.

Stacks are, unfortunately, not currently specified at all; every engine does its own thing. And apart from the stack trace, this sounds exactly like generator.throw(new Error), which already exists.

Engines could choose to put the generator's currently paused yield into the stack trace when .throw(new Error) is called, if they wanted. Would that meet your use case? If so, you should try filing issues on their bug trackers. If not, I don't understand the ask; can you put it a different way?

I know that stacks aren't specified, but I think it's generally safe to assume that they will continue to work basically as they currently do -- create a trace which represents the state of the stack at the location the Error object was constructed. Unfortunately generator.throw(new Error()) doesn't help at all since the error is being constructed outside the generator not inside it.

Furthermore I think now that error.cause is specified this is a situation where it clearly makes sense to allow it to be used. I'd say there should be two errors, causally related. I think what I'd want to write is:

throw new Error(`unknown {action: ${action}}`, { cause: co.throwError() });

@bakkot I think the problem with your suggestion for engines is that it requires an error to be constructed with knowledge of where it will be thrown, which by definition won't have happened yet.

I think it's generally safe to assume that they will continue to work basically as they currently do -- create a trace which represents the state of the stack at the location the Error object was constructed

That's not how they work for async functions, and there's no reason that must be how they work for generators.

I think the problem with your suggestion for engines is that it requires an error to be constructed with knowledge of where it will be thrown

To be clear, my suggestion is that the engine could update the stack for e when .throw(e) is called, not that they somehow read forward to give it the correct stack at instantiation. The stack does not need to be immutable for the lifetime of the object. V8 even has an API for setting the stack of an error object after construction: Error.captureStackTrace(e). There's no theoretical reason that they couldn't make it so that that the throw method on generators updated the stack trace of a passed error object to point to the yield.

Though, perhaps if this is behavior we really want, we ought to file an issue with this proposal, so that the actual .stack property will be auto-updated for all engines once stack-traces are added to the spec.

That would have to be a follow on proposal; all that one can hope to achieve is the massive task of documenting the union and intersection of existing behavior.

1 Like