When you work with a state machine, you often take care of an errors, which may happen on the business logic layer.

1. Error in the action

Let’s consider what happens if an error occurs during the execution of the action:

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
		transitions.withExternal()
				.source(States.BACKLOG)
				.target(States.DONE)
				.action(failAction())
				.event(Events.ROCK_STAR_DOUBLE_TASK);
}

private Action<States, Events> failAction() {
		return context -> {
				throw new RuntimeException("fail in action");
		};
}

Then we make a test:

@Test
public void failInActionTest() {
 	// Precondition
 	Assertions.assertThat(stateMachine.getState().getId())
					 	.isEqualTo(States.BACKLOG);
	// Act
    stateMachine.sendEvent(ROCK_STAR_DOUBLE_TASK);
    // Asserts
    Assertions.assertThat(stateMachine.getState().getId())
              .isEqualTo(States.BACKLOG);
}

Result of the test:

failed action test result

We are trying to make a transition with failed action. And we see this exception in log, but it not thrown to the place of the call. Because the spring state machine handle all of the runtime exceptions. However, the state-machine is still keeps in the source state.

2. Error in a guard check

Let’s try to make exception in a guard.

Add a new transition:

transitions.withExternal()
           .source(States.BACKLOG)
           .target(States.DONE)
           .event(Events.START_FEATURE)
           .guard(failGuard());

and the failed guard:

private Guard<States, Events> failGuard() {
    return context -> {
        throw new RuntimeException("fail in Guard");
    };
}

Now we can write a test:

@Test
public void failInGuardTest() {
    // Act
    stateMachine.sendEvent(START_FEATURE);
    // Asserts
    Assertions.assertThat(stateMachine.getState().getId())
              .isEqualTo(States.BACKLOG);
}

After that we seeing in the log a same behavior:

failed guard

The exception thrown, but we can’t catch it on the place of the call. The state machine didn’t move to a target state, this is expected behavior. The throw of an exception while checking a guard condition - is equals to returned false by guard.

3. Indication of a state machine errors

The spring state machine have a not rich functionality for working with errors. We can set an error in actions or guards, but we can’t get type of the error later. We can only check for an error, but not the type of exception.

Let’s see example:

transitions.withExternal()
           .source(States.BACKLOG)
           .target(States.DONE)
           .action(actionWithInternalError())
           .event(Events.FINISH_FEATURE);

We set the error in this action, by creating a concrete type of an exception:

private Action<States, Events> actionWithInternalError() {
    return context -> {
        context.getStateMachine()
               .setStateMachineError(new RuntimeException("fail in Action"));
    };
}

Let’s try to check it:

@Test
public void internalFailTest() {
    // Check precondition
    Assertions.assertThat(stateMachine.hasStateMachineError()).isFalse();
    // Act
    stateMachine.sendEvent(FINISH_FEATURE);
    // Asserts
    Assertions.assertThat(stateMachine.getState().getId())
              .isEqualTo(States.DONE);

    Assertions.assertThat(stateMachine.hasStateMachineError()).isTrue();
}

This test was successfully passed.

Before we sent the event in the state machine, it did not have errors. And after this transition we got an error by using hasStateMachineError method. However, the state machine successfully moved to the target state.

The interface of StateMachine is not provide any methods for a getting type of exception. It is strange, but you can make workaround for this by using variables of a state machine.

4. Source code of this project on the github