Skip to content

Comments

[core] Add .onError() handler to service#1606

Closed
davidkpiano wants to merge 13 commits intomainfrom
davidkpiano/1605
Closed

[core] Add .onError() handler to service#1606
davidkpiano wants to merge 13 commits intomainfrom
davidkpiano/1605

Conversation

@davidkpiano
Copy link
Member

This PR adds the .onError(...) handler to services:

const service = interpret(machine)
  .onError(err => {
    // err is instanceof Error
    console.error(err.message);
  })
  .start();

This was necessary to test the behavior of tweaking reportUnhandledExceptionOnInvocation to check that originalError is actually an Error, since reject(anything) might not be an Error object, so err.stack would itself throw an error.

Still deciding if we want to add .onError((err, state) => { ... }) or other metadata 🤔

@davidkpiano davidkpiano requested a review from Andarist October 31, 2020 03:30
@changeset-bot
Copy link

changeset-bot bot commented Oct 31, 2020

⚠️ No Changeset found

Latest commit: f2991f0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes changesets to release 1 package
Name Type
xstate Patch

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@davidkpiano davidkpiano marked this pull request as ready for review November 26, 2020 13:34
Copy link
Collaborator

@Andarist Andarist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to define (in the docs) what's the exact purpose of onError. What it should do and what it can't do. Is it useful only for reporting? Can it influence anyhow the machine's lifetime? This seems vastly important for users wanting to use this.

return this;
}

private handleError(errorData: unknown): void {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are only 2 places in which this is currently called:

  • when an error gets caught when we send an error event to itself after catching promise rejection
  • when an action throws during exec

There are still a lot of uncovered situations:

  • errors thrown during guards evaluation
  • errors thrown during service instantiation
  • errors received from machines (should we hook into their onError?) and from observables

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we should handle errors there as well.

@davidkpiano davidkpiano changed the title [xstate] Add .onError() handler to service [core] Add .onError() handler to service Jan 22, 2021
@Andarist
Copy link
Collaborator

Would be great to define the role of this handler as mentioned here: #1606 (review) . We might change the semantics later but would be great to establish what this PR is currently aiming for.

@davidkpiano
Copy link
Member Author

It would be great to define (in the docs) what's the exact purpose of onError. What it should do and what it can't do.

The purpose of onError is to be a mechanism for the parent to subscribe to errors, whether it's error.* events or thrown errors.

Is it useful only for reporting?

It's useful for reporting and reacting to errors.

Can it influence anyhow the machine's lifetime?

No.

@Andarist
Copy link
Collaborator

The purpose of onError is to be a mechanism for the parent to subscribe to errors, whether it's error.* events or thrown errors.

Is it supposed to handle only uncaught/unhandled errors/events or all? I think the best one would be the former - handled stuff would lead to noise. I haven't rechecked right now which is most likely based on the provided implementation - just trying to establish key goals of this PR without looking at the implementation for now.

Is it supposed to only receive errors that originated in the top-level machine or its invoked actors? Or should it (in the future perhaps) be responsible for receiving all uncaught errors from the whole "system" that it represents?

It's useful for reporting and reacting to errors.

How one would react to an error from it? I mean - what action could one take from there? Just kill the machine? Or maybe something else?

@davidkpiano
Copy link
Member Author

The purpose of onError is to be a mechanism for the parent to subscribe to errors, whether it's error.* events or thrown errors.

Is it supposed to handle only uncaught/unhandled errors/events or all? I think the best one would be the former - handled stuff would lead to noise. I haven't rechecked right now which is most likely based on the provided implementation - just trying to establish key goals of this PR without looking at the implementation for now.

Is it supposed to only receive errors that originated in the top-level machine or its invoked actors? Or should it (in the future perhaps) be responsible for receiving all uncaught errors from the whole "system" that it represents?

To make it simple, onError is for all errors (uncaught and error.* event) that originate from the machine, since (according to the SCXML spec) they are the same thing:

Once the SCXML processor has begun executing a well-formed SCXML document, it must signal any errors that occur by raising SCXML events whose names begin with 'error.'.

https://www.w3.org/TR/scxml/#ErrorEvents

Actors don't send errors to the parent by default; that's what escalate() is for (or sendParent(...) if you want to do it manually), but these should work the same -- if an error is sent to the parent, onError listeners will be called with that error.

How one would react to an error from it? I mean - what action could one take from there? Just kill the machine? Or maybe something else?

That's up to the developer. Stop/restart the machine, log the error, etc.

@Andarist
Copy link
Collaborator

I would temporarily park this PR - until I post the error-related RFC, based on our Discord conversations. It has the potential of influencing the overall design of error handling but also this particular API.

@sebinsua
Copy link

sebinsua commented Aug 11, 2021

@davidkpiano I noticed that errors thrown within .onTransition handlers are unhandled. Would this PR make it easier to catch these Service errors? Has the Error RFC been worked on yet? /cc @Andarist

The company I'm currently working for was considering using xstate within a backend Node server and using an onTransition handler to persist state changes into a database. In later versions of Node, unhandled rejections could crash the server. Also, ideally we'd want the server to be able to respond with a non-200 status code if there is an error and to not commit a database transaction if any part of it fails. This would imply that we would need to find a way to wait on the onTransition.

  1. Is it safe to do database inserts within an onTransition handler? Do we just wrap our handler in a try-catch?
  2. Or, should we instead move all database interactions into an invoked service that can explicitly move to a new state on success/fail?

Am I thinking about this in the wrong way?

@davidkpiano
Copy link
Member Author

@davidkpiano I noticed that errors thrown within .onTransition handlers are unhandled. Would this PR make it easier to catch these Service errors? Has the Error RFC been worked on yet? /cc @Andarist

Yes, the error RFC has been recently been made internally, and we will surface it soon.

  1. Is it safe to do database inserts within an onTransition handler? Do we just wrap our handler in a try-catch?

Yes you can; best to wrap in a try/catch 👍

  1. Or, should we instead move all database interactions into an invoked service that can explicitly move to a new state on success/fail?

Currently, whatever is easiest for you all. Ideally, these interactions will be in an invoked service, but the end result is the same: the machine changing state is due to an event, whether those are events sent by a service or externally. Only a minor refactor is needed in the future if you're currently sending them externally and want to move to a service.

@Andarist
Copy link
Collaborator

@sebinsua statelyai/rfcs#4

@Andarist
Copy link
Collaborator

This PR targets main and we don't plan to introduce any new major features to that release line. The error story is going to be revamped on the next branch and will be released in v5.

@Andarist Andarist closed this Jun 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants