-
-
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
Bug: actors leak uncaught errors #4928
Comments
I updated the title to better describe what I think is happening; my hunch is that it can be fixed in the |
It may be worth mentioning why the const machine = setup({
types: {
input: {} as {signal: AbortSignal}
}
}).createMachine({
context: ({input: {signal}}) => {
if (signal.aborted) {
throw new AbortError(signal.reason);
}
}
}); |
It can't be fixed there. This error is thrown synchronously. By the time You can (likely) work around this problem like this: const actor = createActor(TestMachine);
const p = toPromise(actor);
actor.start();
await p; |
@Andarist Not sure I love it, but I can confirm that workaround works: https://stackblitz.com/edit/github-trfney-kh9qur?file=src%2Fmain.ts Might want to update the docs and/or add a warning about this if you don't plan to address it otherwise 😄 |
To be clear, I don't necessarily say I love it either 😉 |
It's not thrown synchronously, though: xstate/packages/core/src/reportUnhandledError.ts Lines 9 to 13 in 3e5424d
UPDATE: nevermind; this doesn't workLooking at this further, I think it could be solved by modifying private _error(err: unknown, sync = false): void {
this._stopProcedure();
this._reportError(err, sync);
// ...
}
private _reportError(err: unknown, sync = false): void {
if (!this.observers.size) {
if (!this._parent) {
if (sync) {
throw err;
}
reportUnhandledError(err);
}
return;
}
// ...
} Back in xstate/packages/core/src/createActor.ts Lines 493 to 495 in 3e5424d
The case 'error':
this._error((this._snapshot as any).error, true);
return this; It's not an elegant API by any means, but will only throw synchronously if the actor is already in an |
Another way to solve it may be to create a transient, internal |
The second suggestion works without gumming up existing error handling: case 'error':
let err: unknown | undefined;
let tempSub: Subscription | undefined;
if (!this.observers.size) {
tempSub = this.subscribe({error: error => {
// avoid the thing that catches errors thrown from error observers
err = error;
}});
}
this._error((this._snapshot as any).error);
tempSub?.unsubscribe();
if (err) {
throw err;
}
return this;
} and: it('throws errors synchronously if no observers are present', () => {
const machine = createMachine({
context: () => {
throw new Error('oh no');
}
});
const actor = createActor(machine);
expect(() => actor.start()).toThrowErrorMatchingInlineSnapshot(`"oh no"`);
}) |
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 actor = createBrokenActor(); const machine = createMachine({ context: () => { throw new Error('egad!'); } }); try { await toPromise(actor.start()); } catch (err) { err.message === 'egad!' // true } ``` Fixes: statelyai#4928
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
@Andarist I'm having a leaked error with a myState: {
entry: [
assign({
registerAssign: ({ spawn }) => spawn('registerAssignActor', { id: 'register-assign' }),
}),
],
exit: [
stopChild('register-assign'),
assign({ registerAssign: registerAssignRef }),
], I'm able to catch the error event with the wildcard |
Are you adding an error handler? actor.subscribe({
error: (err) => {
// handle error
}
});
actor.start(); |
@davidkpiano I'm spawning the actor by reference and assigning it to the context. |
@matheo please share a runnable repro case of the problem. It's hard to tell what you are observing based on a code slice. |
XState version
XState version 5
Description
Given:
error
subscriber is registered with Acontext
assignment function throws an error EE cannot be caught by any mechanism other than a global error listener (e.g.,
window.onerror
orprocess.on('uncaughtException')
.Even if a machine actor is wrapped in
toPromise()
, E is still thrown this way in addition to being rejected out of thePromise
.This behavior does not occur if the machine actor has a subscriber with an
error
listener.I really don't want to create a bunch of subscriptions where
toPromise()
would work for my purposes. 😄Expected result
The error thrown by the
context
initializer should not be thrown in addition to being rejected.Actual result
The "extra" error thrown is only catchable via a global error handler.
Reproduction
https://stackblitz.com/edit/github-trfney?file=src%2Fmain.ts
Additional context
This may be expected behavior. I understand that an actor that is started without an
error
listener and isn't wrapped in aPromise
has no other choice but to throw an error this way; there's nowhere for it to go.In Node.js, an EE will emit an
error
event, which can be listened to. If it is listened to, then the EE won't throw an uncaught exception. However, theonce
wrapper (which is only vaguely analogous totoPromise()
) will also avoid the uncaught exception:That's why I think this is a bug. Like above,
toPromise
should act like a subscriber with anerror
listener.It may also not be unique to the
context
assignment function, either...The text was updated successfully, but these errors were encountered: