assign failed silently during async transition #1485
Replies: 43 comments
-
onError is a synctatic sugar for a transition for receiving an error event from the invoked service - this error happens in the parent machine though and this it shouldnt be triggered in my opinion, even though this might sound counter intuitive at first. As this error has not been handled properly in the machine that has causes it (the parent one here) it should probably fail that machine, stop it, etc. Its part of the bogger story for v5 - we need to design error handling properly, so the rules can be written down and known to people. Changing this right now - in v4 - would be a breaking change. |
Beta Was this translation helpful? Give feedback.
-
I would slightly clarify here. It's not related to only invoking an async function, but effectively anything that turns the machine into an asynchronous service. Notice that by adding
I think that makes sense, but still, you need some async-aware outlet. Anything thrown while service runs asynchronously will be swallowed unless caught on that promise chain directly. |
Beta Was this translation helpful? Give feedback.
-
Or to be precise, it might show up in the console in some cases as The problem is if you run a synchronous test that cannot see the internal promise chain and as such, it cannot wait for it and catch error on it. I think that ultimately the In the React world, they solved this nicely with |
Beta Was this translation helpful? Give feedback.
-
Hm, I'm not sure if I understand your concerns @FredyC , could you clarify them more? From what I understand @connectdotz issue is not really about being able to make test assertions on anything, the test is just written to showcase a particular behavior and the issue itself is about that behavior. Quoting the "expected outcome" by the OP:
|
Beta Was this translation helpful? Give feedback.
-
Well, the OP is a valid use case, but I am pondering on a wider concern here. You see, as long as the machine is synchronous (simple transitions between different states), the service itself stays synchronous, and any error thrown within action propagates synchronously. That means it can effectively stop the machine and die. But at the moment you introduce asynchronicity, the actions that happen to throw can only pass that error to the Promise chain. It can no longer throw it synchronously. These are call stacks and you can clearly see the difference where My general concern here is not about handling these errors but to be able to identify them without them getting lost. It's painful to not know why action failed and you don't know why. Technically it might be enough if the error is just output with |
Beta Was this translation helpful? Give feedback.
-
Could u prepare a repro case of what you are experiencing? I think I might still be missing something. Rejected promises should trigger onError handlers correctly so those wouldnt be lost. |
Beta Was this translation helpful? Give feedback.
-
The codesandbox in OP is already showing this problem. That's where I got that latter stack trace. Ignore |
Beta Was this translation helpful? Give feedback.
-
I see what might be the confusion... The try/catch will never catch the async errors, unless like you said if the
I guess it depends, if I translate the invoke into the following structure, the
But of course, if |
Beta Was this translation helpful? Give feedback.
-
This doesn't make a conceptual sense though.
Some sort of such behavior would be most desired I believe.
So actually... no error should be even possible to be caught synchronously by a catch block wrapping the |
Beta Was this translation helpful? Give feedback.
-
I am glad we are finally on the same page 😅 For V4 it might make sense a quick patch where error caught in actions is at least logged with |
Beta Was this translation helpful? Give feedback.
-
But arent those logged automatically? Those are just uncaught errors right now so should be logged by the browser. I've adjusted the @connectdotz codesandbox' to switch to the sync mode and that error is being logged: |
Beta Was this translation helpful? Give feedback.
-
Oh wait, you have to open devtools console in CS to see that. I guess it would be seen in the normal browser as well. But it's wildly inconsistent. Sync error is in CS console, async in devtools only. It's easy to miss that. |
Beta Was this translation helpful? Give feedback.
-
That has to be some CS quirk though, the error is logged correctly to the browser's console in both cases and if we'd add an additional |
Beta Was this translation helpful? Give feedback.
-
@connectdotz I wonder what was your initial motivation for this? I mean you said it's not about being able to catch that error in the test but to know about an unexpected error that happened in the action. Didn't you get that error spilled in the console? |
Beta Was this translation helpful? Give feedback.
-
It's a good question why the
I took a quick look at Jest did not do that so we won't know there is an uncaught exception for async operations during testing. Sure we can defer the responsibility to the other runtime containers or ask our users to deal with it themselves (with enough console.log, one can find the problem eventually)... But I think if xstate can detect and help users to quickly realize and pinpoint the problem, without relying on the underlying engine, it will save everybody a lot of time thus make xstate a much better framework... regarding double reporting, if these errors are reported to the optional |
Beta Was this translation helpful? Give feedback.
-
Never heard the term "silent logging" :) I suppose you mean that it doesn't stop anything and code around is moving forward. However, errors in actions are already behaving like that since you cannot catch them. In some environments (why this issue exists) it's even more painful to rely on "unhandledRejection" to appear somewhere.
I have no problem with that, that will certainly help in many cases. My concern is that you need to explicitly set it up for every machine. Not many people will realize they need to do that. Unless of course, you plan to add an implicit handler to |
Beta Was this translation helpful? Give feedback.
-
This is what I'm confused about. You cannot treat async errors as sync (i.e., via So let's resolve on adding an Unless you can demonstrate what converting an async error to a sync one would look like?
Sure, this can be done.
We shouldn't define custom states for this. The interpreter should stop. But the error should still be "visible" - we shouldn't silence the error. The relevant part of the SCXML spec is here: https://www.w3.org/TR/scxml/#ErrorEvents
So here is my proposal:
|
Beta Was this translation helpful? Give feedback.
-
@davidkpiano a few questions:
|
Beta Was this translation helpful? Give feedback.
-
An async error is an event like any other. If it is not handled (i.e., in
Yeah, that's reasonable. We should add logging.
The createMachine({
// ...
onError: {
actions: (_, e) => console.error(e),
target: '.someFinalState'
}
}); You're right; as is described in SCXML, adding an explicit "error handler" transition should be preferred. |
Beta Was this translation helpful? Give feedback.
-
It would certainly be nice if we could explicitly transition to an error state no matter where the error happened in the machine. I suppose the Also ... what happens if error handler has error? :) Should be covered somehow so we don't end up in the loop. |
Beta Was this translation helpful? Give feedback.
-
Yes, that would work with the top-level
Sigh, one thing at a time 😅 I'll consider that too. |
Beta Was this translation helpful? Give feedback.
-
And what about currently available I would say that if a specific handler is declared, it should be only one to run. Otherwise, it would fallback to top-level one. |
Beta Was this translation helpful? Give feedback.
-
The transition resolution follows the exact same semantics as SCXML. It goes from leaf-node to root-node; most-specific to least-specific. If an error event is handled in a state, it will only be handled by that transition. Otherwise, it propagates to the nearest ancestor that handles the error. |
Beta Was this translation helpful? Give feedback.
-
@davidkpiano I think the idea of making
Therefore, while I don't object |
Beta Was this translation helpful? Give feedback.
-
@connectdotz I think it's generally correct to have at least one error state in each machine (except simple ones) used whenever something goes wrong. You can even have "retry" logic implemented with that. Also if I understand correctly from the above snippet, it won't be mandatory to specify |
Beta Was this translation helpful? Give feedback.
-
Right; let's not complicate this. The scope is getting too large, so for v4, let's limit it to this:
const machine = createMachine({
// ...
states: {/* ... */},
onError: {
// top-level error catcher
}
});
Again, XState is about being explicit, so if you don't explicitly add a transition to capture error events, then the normal (arguably undesirable) behavior should happen. |
Beta Was this translation helpful? Give feedback.
-
Without the target the machine will not transition to a final state (exit) and thus remain in the wrong state, isn't it? Isn't this directly violate SCXML recommendation that it should lead to exit/final state (to maintain the state machine integrity)?
indeed, introducing
It seems this is the main reason that @davidkpiano feels strongly against a new system-state approach... I can see your point but let me make one last argument 😅 ... it might be actually desirable to make an exception for the uncaught-error state, which is mostly a development-stage scenario, so the machine definition won't need to be polluted... ok, that's all I am going to say about this issue 🤐 . Whatever you guys decided to do that is better than what we have today is great. I appreciate your patient to discuss and continue to improve this product. 🙏 |
Beta Was this translation helpful? Give feedback.
-
It's a recommendation; it's up to the developer to specify a final state target if they feel it's appropriate.
No exceptions. In the case that an error is not caught by any
Thanks! I'll make a PR and we can bikeshed later, especially for v5. |
Beta Was this translation helpful? Give feedback.
-
I must say that I still don't quite understand the whole discussion about adding logging for uncaught errors. From what I've checked this is exactly how it behaves right now - because it's the default behavior of most environments. I'm not aware of any modern environment that swallows
Right, it's a recommendation in the SCXML but - as @connectdotz has noticed - this might not be enough for our default. I feel that recommendation favors unsafe behavior over safer ones. If any error is left without being handled by a specific user handler then the model is broken - repercussions are unknown and they might be severe for the application, user flow, or whatever. It's by far too easy to forget adding a proper error handling and thus leave your machines in the inconsistent states if we don't stop such broken machines from executing anything further. But also you have expressed similar desire here:
The whole conversation became lengthy and quite hard to track for me so I'm not even sure what are exact, final, opinions of all people involved here 😅 |
Beta Was this translation helpful? Give feedback.
-
Yeah, let's move this to discussions instead - there is nothing that is clearly actionable here. (EDIT: moved!) |
Beta Was this translation helpful? Give feedback.
-
Description
when invoking an async function (returns a promise), upon
onDone()
, it tried to update the context with the promise result. If for some reason the code failed (unintended, such as a coding error), the error is not be captured nor reported (at least during testing, possibly in a production run as well)see https://codesandbox.io/s/silent-failure-example2-g0p9d test run, the
async operation should report error
test case failed not by exception but by timing out (xstate is not advancing the state nor reporting error)Expected Result
either invoke
onError()
of the invoke or terminate the interpreter as the sync-operation does.Actual Result
the interpreter does not advance state nor reporting error.
Reproduction
https://codesandbox.io/s/silent-failure-example2-g0p9d
Additional context
original gitter discussion: https://gitter.im/statecharts/statecharts?at=5f6a14c64002c640b5dd60b3
Beta Was this translation helpful? Give feedback.
All reactions