diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index 3ad250edcc..234362e989 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -491,7 +491,26 @@ export class Actor // 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; } diff --git a/packages/core/test/errors.test.ts b/packages/core/test/errors.test.ts index 0a6cb0d1a1..200cade4f3 100644 --- a/packages/core/test/errors.test.ts +++ b/packages/core/test/errors.test.ts @@ -5,7 +5,8 @@ import { createMachine, fromCallback, fromPromise, - fromTransition + fromTransition, + toPromise } from '../src'; const cleanups: (() => void)[] = []; @@ -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'); + } + }); });