Skip to content

Commit

Permalink
fix(core): errored initial snapshot throws sync w/o observers
Browse files Browse the repository at this point in the history
This modifies the error-handling logic for when an error is thrown during the creation of an Actor's initial snapshot (during `start()`).  _If_ the actor has _no_ observers, the error will now be thrown synchronously out of `start()` instead of to the global error handler.

Example use case:

```js
const machine = createMachine({
  context: () => {
    throw new Error('egad!');
  }
});

const actor = createActor(machine);

try {
  await toPromise(actor.start());
} catch (err) {
  err.message === 'egad!' // true
}
```

Note that this _does not impact child actors_.

Fixes: #4928
  • Loading branch information
boneskull committed Aug 17, 2024
1 parent 3e5424d commit 0dabaa7
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 1 deletion.
19 changes: 19 additions & 0 deletions packages/core/src/createActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,26 @@ export class Actor<TLogic extends AnyActorLogic>
// TODO: rethink cleanup of observers, mailbox, etc
return this;
case 'error':
// in this case, creation of the initial snapshot caused an error.

// **if the actor has no observer when start() is called**, this error would otherwise be
// thrown to the global error handler. to prevent this--and allow start() to be wrapped in try/catch--we
// create a temporary observer that receives the error (via _reportError()) and rethrows it from this call stack.
let err: unknown;
let errorTrapSub: Subscription | undefined;
if (!this.observers.size) {
errorTrapSub = this.subscribe({
error: (error) => {
// we cannot throw here because it would be caught elsewhere and rethrown as unhandled
err = error;
}
});
}
this._error((this._snapshot as any).error);
errorTrapSub?.unsubscribe();
if (err) {
throw err;
}
return this;
}

Expand Down
40 changes: 39 additions & 1 deletion packages/core/test/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
createMachine,
fromCallback,
fromPromise,
fromTransition
fromTransition,
toPromise
} from '../src';

const cleanups: (() => void)[] = [];
Expand Down Expand Up @@ -892,4 +893,41 @@ describe('error handling', () => {
error_thrown_in_guard_when_transitioning]
`);
});

it('error thrown when resolving the initial context should rethrow synchronously', (done) => {
const machine = createMachine({
context: () => {
throw new Error('oh no');
}
});

const actor = createActor(machine);

installGlobalOnErrorHandler(() => {
done.fail();
});

expect(() => actor.start()).toThrowErrorMatchingInlineSnapshot(`"oh no"`);

setTimeout(() => {
done();
}, 10);
});

it('error thrown when resolving the initial context should reject when wrapped in a Promise', async () => {
const machine = createMachine({
context: () => {
throw new Error('oh no');
}
});

const actor = createActor(machine);

try {
await toPromise(actor.start());
fail();
} catch (err) {
expect((err as Error).message).toEqual('oh no');
}
});
});

0 comments on commit 0dabaa7

Please sign in to comment.