Dumont Digital logo

How to throw an error from an invoked child machine to a parent in xstate

The basic syntax for invoking a child machine is the same as for invoking promises or any other service:

// ...
current_state: {
  invoke: {
    src: childMachine,
    data: {}, // optional initial context
    onDone: {
      target: 'next_state',
      actions: 'someAction',
    },
    onError: {
      target: 'error',
      actions: 'printError',
    }
  },
},

When you invoke a promise, an error thrown inside will automatically trigger the onError block. You could expect the same thing to happen when invoking a child machine and throw an error like so:

// ...
states: {
  fetch: {
    invoke: {
      src: 'fetchSomething',
      onDone: { target: 'upsert', actions: 'someAction' },
      onError: {
        actions: (_, event) => {
          throw event.data
        },
      },
    },
  },
  upsert: {
    // ...
  },
  success: {
    type: 'final',
  },
},

The problem

However, this doesn’t work, and the “Invoking services” docs only mention sendParent as a way to communicate from child to parent machine.

You could use sendParent to solve the issue, but you’d need to send a custom event from your child machine and handle it in the parent, outside of the onError block:

// ... child machine
actions: {
  throwError: sendParent((_, event) => ({
    type: 'CHILD_ERROR',
    data: event.data,
  })),
},

// ... parent machine
current_state: {
  invoke: {
    src: childMachine,
    data: { /* ... */ },
    onDone: { /* ... */ },
  },
  on: {
    CHILD_ERROR: {
      target: 'error',
      actions: 'printError',
    },
  },
},

That’s actually what I did until a friendly discord user pointed me in the right direction.

The solution

Turns out the answer was in the “Actions” page of the docs, under the escalate action, which “escalates an error by sending it to the parent machine.” Exactly what we need.

Here’s how you’d refactor the throwError action so that it would trigger the onError block of the parent:

// ...
throwError: escalate((_, event) => ({
  data: event.data,
})),

© 2022 freddydumont