-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(core): errored initial snapshot throws sync w/o observers #5045
Conversation
|
For reference, the current workaround is this: const machine = createMachine({
context: () => {
throw new Error('egad!');
}
});
const actor = createActor(machine);
const p = toPromise(actor);
actor.start();
try {
await p;
} catch (err) {
err.message === 'egad!' // true
} |
I don't know if the new behavior is desirable to the maintainers, but I find it more ergonomic. Thought I would restart that conversation here! |
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: statelyai#4928
07acff0
to
0dabaa7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with this PR in principle; waiting to see what @Andarist thinks
My main concern is that it is somewhat inconsistent, since if the example machine was spawned as a child actor, it would reach the global error handler. ...that being said, if using |
This looks like a breaking change to me unless you feel this is actually a "bug fix" and not just a "revision of intent" 😜 |
Could you show the exact situation you are talking about here? |
something like this: it('handled sync errors thrown when starting a child actor should be reported globally when spawned via spawnChild()', (done) => {
const badMachine = createMachine({
context: () => {
throw new Error('handled_sync_error_in_actor_start');
}
});
const machine = createMachine({
entry: [
spawnChild(badMachine)
]
});
const actorRef = createActor(machine);
actorRef.start();
installGlobalOnErrorHandler((ev) => {
ev.preventDefault();
expect(ev.error.message).toEqual('handled_sync_error_in_actor_start');
done();
});
}); If a |
// **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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we can do this a different way without creating a temp sub
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidkpiano The folded section in this comment has an alternate proposal, which I explored but didn't get working fully.
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'); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't work like you think it does. This error is still very much thrown synchronously. toPromise
isn't ever called during this test case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you mean. If createActor
threw synchronously, wouldn't this test fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, It's the actor.start
that throws, before its return value can even get passed to toPromise
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"`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actors are always async conceptually, a synchronous throw is something that might sound convenient at first but you essentially create 2 error channels by introducing this. Now, if you want to handle errors, u need to both install an error listener and wrap this in try/catch. The guiding principle that we had when designing this was that you should always be notified about errors in the very same way (through error listeners).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Andarist In theory I agree, but in practice, if we know that an actor is going to fail right at the start, rather than later during event processing, it's a much better DX (IMO) to throw right away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my feeling too. User experience > theoretical purity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@boneskull I may have changed my stance on this: https://www.youtube.com/watch?v=JCXZhe6KsxQ&t=294s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't follow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidkpiano Should I close this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, let's close it for now, given that it's a breaking change.
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 ofstart()
instead of to the global error handler.Example use case:
Note that this does not impact child actors, but it might want to!
Fixes: #4928