0
votes

I am new to Axon framework and am trying to implement an application using CQRS with state-stored aggregates. The application relies on a database constraint (using H2 for now) to enforce uniqueness on a name attribute. I would like to catch this exception and rethrow it as a user-friendly domain exception.

Per the Axon documentation:

  • Exception Handling says "an @ExceptionHandler will only handle exceptions thrown from message handling functions in the same class"
  • Message Intercepting documentation says "A function annotated with @ExceptionHandler will be regarded as a handler interceptor which will only be invoked for exceptional results. Using annotated functions to this end for example allow you to throw a more domain specific exception as a result of a thrown database/service exception."

But I cannot get this to work. I have tried adding exception handlers as follows:

    @ExceptionHandler
    public void handle(ConstraintViolationException ex) throws Exception {
        if (ex.getMessage().contains("UNQ_COMPANY_ID") || ex.getMessage().contains("UNQ_PLAN_NAME")) {
            throw new DomainException("Plan name and company id must be unique");
        }
        throw ex;
    }

but this method is not called. I have tried putting the exception handler method on the aggregate and on a separate command handler class, tried adding resultType=ConstraintViolationException.class, and tried catching other types of exceptions including Exception, RuntimeException, AxonServerRemoteCommandHandlingException, etc. but this method is never called.

I can see the error in the log output:

org.axonframework.axonserver.connector.command.AxonServerRemoteCommandHandlingException: An exception was thrown by the remote message handling component: org.hibernate.exception.ConstraintViolationException: could not execute statement

Is it possible to catch database exceptions in state-stored aggregates? If it is, can someone point me towards what I am doing wrong?

The statement "an @ExceptionHandler will only handle exceptions thrown from message handling functions in the same class" makes me wonder whether I need to create a custom repository class (rather than using the default GenericJpaRepository) but that seems like a lot more work than should be necessary.

Thank you!


Update: I was able to roughly accomplish what I want by adding a UnitOfWork parameter to the @CommandHandler method and using it to registering a rollback callback on it as follows:

        uow.onRollback(unit -> {
            DefaultUnitOfWork duow = (DefaultUnitOfWork) unit;
            Throwable ex = duow.getExecutionResult().getExceptionResult();
            while (ex != null) {
                if (ex.getMessage().contains("UNQ_PLAN_NAME")) {
                    throw new RuntimeException("Plan name must be unique");
                }
                ex = ex.getCause();
            }
        });

But this seems kind of verbose, as well as limiting me to throwing unchecked exceptions only. This also doesn't feel like the right way to do this though because I assume the purpose of the @ExceptionHandler annotation is to eliminate need for code like the above.

2

2 Answers

1
votes

This is doable of course.

Actually, the best pointer I could give you if the code-samples repo where you can see a sample about distributed exceptions.

In general, as you could see in your shared log, the 'original' exception is wrapped into an AxonServerRemoteCommandHandlingException meaning you will have to handle that. Doing that, you can pretty much add anything to the details field of this class, adding the indication you had a ConstraintViolationException for example (or an ERROR_CODE, like HTTP protocol does) and you are fine to unwrap it on the other side.

0
votes

What might be the "gotcha" you require, is to know that an @ExceptionHandler annotated method should reside in the object handling the message. So if you want to react to a failing command handling operation (which would be the case in your sample), you will need to place the exception handler in the Aggregate, next to the Command Handler.

That fact you get an AxonServerRemoteCommandHandlingException to me suggests the exception is caught on the command dispatching end. So, prior to dispatching a command on the CommandGateway/CommandBus.

However, whether this is the problem at hand, isn't clear to me right now, since the sample only shows the exception handler and not where it resides. Please share whether my assumption on the placement of the exception handler is correct. If not, we'll dive into this deeper to get to the cause.