How Axon Exceptions can be customized?

Hello,

I am new to the Axon Framework and am currently investigating how to customize the standard Axon exceptions. Specifically, I have encountered the following issue: when I send a command to an aggregate that does not exist, I receive the error message:

org.axonframework.modelling.command.AggregateNotFoundException: The aggregate was not found in the event store

This message is then returned to the client. I am looking for a way to capture and customize this message.

Note: It’s a Spring Webflux app

I manage to kinda make it work with below config, but it’s too generic.

  public void registerCommandGatewayResultInterceptor(ReactorCommandGateway commandGateway) {
    commandGateway.registerResultHandlerInterceptor((msg, results) -> results.onErrorMap(e -> {
      if (e instanceof CommandExecutionException exception) {
            return ....
      }
      return new CustomException(...);
    }));
  }

but it doesn’t fully satisfy me because It’s not clear to which aggregate the exception is related to. I tried with method in aggregate root, annotated with @CommandHandlerInterceptor but it doesn’t work, doesn’t intercept.

@CommandHandlerInterceptor
  public void intercept(SomeCommandExample command, InterceptorChain interceptorChain) {
    try {
      interceptorChain.proceed();
    } catch (Throwable throwable) {
      throw new CustomException(....);
    }
  }

The problem is that the AggregateNotFoundException occurs before the Aggregate’s command handler is reached. Hence, using the @CommandHandlerInterceptor or @ExceptionHandler annotation inside the aggregate class will not work.

To be able to wrap this exception, you can either:

  1. Implement the MessageHandlerInterceptor or MessageDispatchInterceptor that customizes the exception and configure it on the CommandBus.
  2. Construct a “Command Handling Component” that contains the @CommandHandler annotated methods, delegating the operation to the aggregate instance by invoking the Repository. By doing so, you are able to use the @CommandHandlerInterceptor/@ExceptionHandler annotation in the Command Handling Component.

Given that you’re dealing with the AggregateNotFoundException, an that can occur for every command, I think the generic MessageHandlerInterceptor/MessageDispatchInterceptor would be a good fit.

As soon as the exceptions are model specific (so, exception your implementation throws instead of Axon Framework), I would indeed go for an aggregate-specific solution with the @CommandHandlerInterceptor/@ExceptionHandler.

By the way, if you want to know more about exception handler annotated methods, I recommend you to check this part of our documentation.

Lastly, if you have any more questions concerning this point, be sure to reply!

Thanks for your answer! I managed to get it working with a generic MessageHandlerInterceptor. However, I had to change from ReactorCommandGateway to the non-reactive CommandGateway to make it work.

I attempted to use reactorCommandGateway.registerResultHandlerInterceptor, but when I tried to create exception details based on the exception type, I was not able to differentiate it as it’s done here: ExceptionWrappingHandlerInterceptor example.

Is it possible to achieve similar behavior with ReactorCommandGateway?

Ah you’re using a ReactorCommandGateway!

Just checking, but did you create a ReactorMessageDispatchInterceptor/ReactorResultHandlerInterceptor?
For intercepting messages on the Reactor gateways, you need to use the aforementioned interceptor interface.

I tried with ReactorResultHandlerInterceptor but the method responsible for creating exception details do not work correctly.

private GiftCardBusinessError exceptionDetails(Throwable throwable) {

    // alternatively this can be a centralised place to do a check on exception's more specific instance
    // and populate the details accordingly (code, message, etc), instead of relying on the Domain Exception
    // to contain all the information and mapping logic
    if (throwable instanceof GiftCardException gce) {
        GiftCardBusinessError businessError = new GiftCardBusinessError(
                gce.getClass().getName(), gce.getErrorCode(), gce.getErrorMessage()
        );
        logger.info("Converted GiftCardException to " + businessError);
        return businessError;
    } else {
        GiftCardBusinessError businessError = new GiftCardBusinessError(
                throwable.getClass().getName(), GiftCardBusinessErrorCode.UNKNOWN, throwable.getMessage()
        );
        logger.info("Converted CommandExecutionException to " + businessError);
        return businessError;
    }
}

Each exception in that method was CommandExectuionException and I was not able to treat accordingly different exceptions from an aggregate.

Hmm, that is curious.

So, this exceptionDetails(Throwable) is invoked if you use the MessageHandlerInterceptor, but is not invoked when you use a ReactorResultHandlerInterceptor?

Could you perhaps share how you’ve constructed the interceptors themselves? If that is something you can share, of course.

@Autowired
void commandBus(ReactorCommandGateway reactiveGateway) {
    reactiveGateway.registerResultHandlerInterceptor(
        (msg, results) -> results
            .onErrorMap(error -> new CommandExecutionException(
                "An exception has occurred during command execution", error,
                exceptionDetails(error, msg.getIdentifier())
            ))
    );
}

private BusinessErrorDetails exceptionDetails(Throwable throwable, String entityName) {
    if (throwable instanceof CustomBusinessException customBusinessException) {
        return new BusinessErrorDetails(
            customBusinessException.getClass().getName(), customBusinessException.getHttpStatus(),
            customBusinessException.getErrorCode(), customBusinessException.getErrorMessage()
        );
    } else if (throwable instanceof AggregateNotFoundException) {
        return new BusinessErrorDetails(
            throwable.getClass().getName(), HttpStatus.NOT_FOUND, null,
            ENTITY_NOT_FOUND_MESSAGE.formatted(entityName)
        );
    } else {
        return new BusinessErrorDetails(
            throwable.getClass().getName(), HttpStatus.INTERNAL_SERVER_ERROR, null,
            SERVER_ERROR_MESSAGE
        );
    }
}

With the above configuration, each time it goes to the else branch as it’s not able to detect the custom exception thrown from an aggregate. The throwable is an “org.axonframework.commandhandling.CommandExecutionException: The remote handler threw an exception” with no underlying cause, etc., except string details.

However, with the non-reactive implementation below, it works as expected and I’m able to differentiate between different exceptions and thus create exception details, before throwing CommandExecutionException.

@Override
public Object handle(UnitOfWork<? extends CommandMessage<?>> unitOfWork, InterceptorChain interceptorChain) {
    try {
        return interceptorChain.proceed();
    } catch (Throwable e) {
        throw new CommandExecutionException("An exception has occurred during command execution", e, exceptionDetails(e, "entityName"));
    }
}

private BusinessErrorDetails exceptionDetails(Throwable throwable, String entityName) {
    if (throwable instanceof CustomBusinessException customBusinessException) {
        return new BusinessErrorDetails(
            customBusinessException.getClass().getName(), customBusinessException.getHttpStatus(),
            customBusinessException.getErrorCode(), customBusinessException.getErrorMessage()
        );
    } else if (throwable instanceof AggregateNotFoundException) {
        return new BusinessErrorDetails(
            throwable.getClass().getName(), HttpStatus.NOT_FOUND, null,
            ENTITY_NOT_FOUND_MESSAGE.formatted(entityName)
        );
    } else {
        return new BusinessErrorDetails(
            throwable.getClass().getName(), HttpStatus.INTERNAL_SERVER_ERROR, null, SERVER_ERROR_MESSAGE
        );
    }
}

That is still quite interesting actually, as both should end up with a CommandExecutionException. What kind of Command Bus are you using, @D3ska? Axon Server perhaps?

On any note, the use of the CommandExecutionException, also for your custom exceptions, is recommended when you are executing in a distributed environment.

Axon Framework will, by default, wrap the thrown exception into a so-called HandlerExecutionException before it sends reply back over the wire.
Why? Well Axon Framework cannot be certain that the throw exception is a known type on the receiving end of the reply.

If it would simply assume that, applications that don’t share the exception classes would break with a serialization exception. This causes the issue to be even more opaque.

To make the solution a bit more concrete, I would recommend to let the CustomBusinessException be an implementation of the CommandExecutionException. Or, to catch the CustomBusinessException inside the Aggregate, so before it went over the wire, and map it to a CommandExecutionException with specific details.
There’s also a recording (of me) that shows how to deal with exceptions in your message handling components, which you can watch here. If you prefer sample code, you can also check out the project I’m showcasing in that video over here.
Lastly, our documentation has this to say about it throwing exceptions from message handlers.

Now, don’t get me wrong, I still think it is weird you don’t get a CommandExecutionException when you are not using the reactive interceptors. Hence my original question of what kind of Command Bus you are using in your environment.

1 Like

Thanks a lot for the detailed answer.
I have one more question, though. I would like to catch and handle the AggregateNotFoundException, which is not handled by @ExceptionHandler since the aggregate is not yet resolved. In such a scenario, external command handler is the only option or I can achieve it in different way? Basically I want to return the message to the client “<aggregate_name> was not found” instead of Axon internal AggregateNotFoundException message.

Regarding the Command Bus, it’s JGroups.

To catch the AggregateNotFoundException you need to have an interceptor before being in the instance.
The @ExceptionHandler can only be invoked once the instance is found.

Given the type of exception, the instance in question doesn’t exist yet, so simply cannot be caught by an “instance-based interceptor” like the @ExceptionHandler/@MessageHandlerInterceptor.

Hence to that end, either implementing the aforementioned interceptor interfaces or adding a try-catch block around the CommandGateway#send/CommandGateway#sendAndWait operation would be the way forward, @D3ska.

Hoping to have informed you sufficiently by now!

1 Like