How to catch custom exceptions inside REST controller / why doesn't Axon propagate custom exceptions?

I built a simple Spring Boot REST API using Kotlin and the Axon Framework, patterned off of the “Food Ordering App” example on YouTube. Then came the time to return a 400 response from my POST /api/user/register endpoint when a client tries to create a new account with a username that’s already in use. In other words, I needed to handle business-specific error cases and inform the client of what went wrong.

So, just like Steven did in the above video, I created a new custom exception class called UsernameAlreadyTakenException, and threw it inside my create user @CommandHandler function if the username was already in use. I expected that I would then be able to catch (e: UsernameAlreadyTakenException) { ... } from my REST controller’s endpoint method, and return the 400 response containing the exception’s message.

But as I learned, for reasons that I don’t totally understand, the Axon Framework intentionally does not propagate custom exceptions up the stack, and instead wraps / converts them to CommandExecutionException which contains a details field where developers are to put all of their custom exception error codes, messages, etc. The docs explain it this way:

Due to the application division, you loose any certainty that both applications can access the same classes, which thus holds for any exception classes.

On the surface, this seems like a reasonable explanation. But the more I think about it, the more it doesn’t make sense to me. If “the thing” that publishes a command and “the thing” that handles that command are separate applications and you’re worried about them not having a particular class on their respective paths, then… can’t you simply add that class to both paths and be done with it?

How is storing custom exception logic like error codes and error messages inside the details field of CommandExecutionException better than just adding MyCustomException to both applications’ class paths? If the command handler application stores an error code inside of details.errorCode and an error message inside details.errorMessage, then the command publisher still has to extract those fields individually by doing getDetails().errorCode and getDetails().errorMessage. So you still have a “contract” between the command publisher application and the event handler application, but now the details of this contract are opaque. It would’ve been better if you could just catch MyCustomException from the command publisher application, and be able to look at the definition of MyCustomException to see what fields it gives you.

Let me illustrate what I mean with some code. Here’s my solution for throwing custom exceptions inside of command handlers and having them trigger different HTTP responses:

UserManagementController.kt:

@RestController
@RequestMapping("api/user")
class UserManagementController(
    val commandGateway: CommandGateway,
    val queryGateway: QueryGateway
) {
    @PostMapping("register")
    fun createUser(
        @RequestBody createUserReqBody: CreateUserRequest
    ): CompletableFuture<ResponseEntity<Map<String, String>>> {
        return commandGateway.send<UUID>(
            CreateUserCommand(
                createUserReqBody.username,
                createUserReqBody.password
            )
        ).thenApply { ResponseEntity.ok(mapOf("userId" to it.toString())) }
    }

    // other endpoints...
}

User.kt:

@Aggregate
data class User(
    @AggregateIdentifier
    var userId: UUID? = null,
    var username: String? = null,
    var password: String? = null,
) {
    constructor() : this(null, null, null)

    @CommandHandler
    constructor(
        command: CreateUserCommand,
        userService: UserService
    ) : this(UUID.randomUUID(), command.username, command.password) {
        if (userService.usernameAlreadyTaken(command.username)) {
            throw UsernameAlreadyTakenException()
        }
        AggregateLifecycle.apply(
            UserCreatedEvent(
                userId!!,
                command.username,
                command.password
            )
        )
    }

    // other handlers...
}

exceptions.kt:

data class RestExceptionDetails(
    val message: String,
    val httpCode: HttpStatus
)

// So as you can see, your custom exception essentially becomes a POJO:
class UsernameAlreadyTakenException() : CommandExecutionException(
    // Also notice how the various fields of `CommandExecutionException`
    // are basically useless here:
    null,
    null,
    RestExceptionDetails(
        "This username is already taken",
        HttpStatus.BAD_REQUEST
    )
)

RestExceptionHandling.kt:

@RestControllerAdvice(assignableTypes = [UserManagementController::class])
class RestExceptionHandling {
    @ExceptionHandler(CommandExecutionException::class)
    fun handleRestExceptions(
        e: CommandExecutionException
    ): ResponseEntity<Map<String, Any>> {
        val details: RestExceptionDetails? = e
            .getDetails<RestExceptionDetails>().orElse(null)
        if (details == null) {
            throw e
        }
        // Your custom fields can now finally change which HTTP responses
        // get sent back to the client (e.g.: 400, 404, etc.)
        return ResponseEntity.status(details.httpCode).body(
            mapOf("error" to details.message)
        )
    }
}

On a related note, I can see that I’m not the first person to be confused by this functionality (one, two, three, four). In my opinion, this situation could be improved by either:

  • Expanding the Axon documentation on exception handling, adding some minimal code examples like the ones above, or
  • Simply propagate the custom exception, which is clearly the behaviour that a lot of users besides myself are expecting out of the box, and leave it up to the “catching” application to have all necessary classes on its path

Just checking in, but I assume this post is a duplicate of this StackOverflow post.

Is that a correct assumption, @0xFFFF_FFFF?

Hi @Steven_van_Beelen, thanks for the reply. This forum post here is similar to the StackOverflow question that you linked (also written by me), except here I was more interested in asking why this is the current behaviour in Axon, i.e.: why not just add MyCustomException to all application’s class paths, instead of accessing the custom exception as a details field of CommandExecutionException?

Ah, gotcha @0xFFFF_FFFF. Thanks for clarifying!

In all honesty, I was reading the first bit of this post and recognized the SO thread.
Hence, I made an assumption it was a full duplicate.

So, assuming that users can find the referenced StackOverflow thread containing the solution, let’s move on to the open question that’s left.

From the perspective of Axon Framework, the concrete type of exception is a result that’s not accounted for when dispatching a command.
Hence, if you expect that type to be present on the classpath of the command dispatching application, you can receive the (even less helpful) ClassNotFoundException.

To combat that, Axon Framework will upfront adjust the thrown Exception in the message handler into the aforementioned CommandExecutionException (or QueryExecutionException for query handlers).

Do note that this will only happen if you use a distributed version of the CommandBus and/or QueryBus.
Because only in those scenarios will the result flow back to a dispatcher that may have different classes on the classpath.

To still be able to define a form of “exceptional (result) API” by the user, we introduced the Object details field in both execution exceptions.
They’re an Object on purpose so that you have the freedom to (e.g.) use dedicated error responses.

By the way, the specific example of using error codes in the details object, fed through using Axon’s @ExceptionHandler annotation, is something I show in this recording.

Does that explain the current behavior sufficiently for you, @0xFFFF_FFFF?
If there are any questions or concerns left, be sure to share them!

1 Like

Thanks for the reply, @Steven_van_Beelen, it’s great to get more insight on how this all works! Some follow-up questions that I have after reading your reply:

Question 1:

this will only happen if you use a distributed version of the CommandBus and/or QueryBus

I am actually seeing this behaviour (i.e. custom exceptions being converted to CommandExecutionException) in my demo application (GitHub link) which I created using the Axon Initializr tool, with minimal other configurations. So does this mean that my app is using a distributed version of CommandBus? How can I tell?

Question 2:

From the perspective of Axon Framework, the concrete type of exception is a result that’s not accounted for when dispatching a command.
Hence, if you expect that type to be present on the classpath of the command dispatching application, you can receive the (even less helpful) ClassNotFoundException.

My question is: if throwing, say, UsernameAlreadyTakenException will cause a ClassNotFoundException on the catching side, then why not simply add UsernameAlreadyTakenException to the classpath on both sides and be done with it? This seems like an easy fix in my mind.

Question 3:

To still be able to define a form of “exceptional (result) API” by the user, we introduced the Object details field in both execution exceptions.

I understand that the “catching side” can access any custom exception information that it needs via the details object (I am doing precisely this in my code snippets above), but doesn’t doing so constitute an implicit contract between the catching side and the throwing side? The catching side has to “know” that the details object contains, say, httpCode. In my opinion, it would’ve been better if each side of the application simply added UsernameAlreadyTakenException to their classpaths, and then the contract between “throwing side” and “catching side” would be plainly visible. Not only would it be easier to catch the custom exception, but now the developer can simply go to the definition of UsernameAlreadyTakenException and see all of its custom fields for themselves. There’s no need to just “know” to look for a httpCode field in the details object.

Because otherwise, how does “catching side” even know that the httpCode field exists in the details object in the first place? A developer would have to have access to the code on the “throwing side”, and look to see if UsernameAlreadyTakenException is adding any custom fields to the details object. And if a developer already has access to both sides of the code, then the question is, why not simply add UsernameAlreadyTakenException to the classpath of the catching side?

I understand that propagating custom exceptions is not how the Axon Framework works currently. But in my opinion, it would be better if it did work this way, for the following reasons:

  • less developer confusion; propagating custom exceptions seems to be the behaviour that developers expect out of the box (see examples linked at the bottom of my original post above)
  • we can solve the ClassNotFoundException problem by simply adding the class to the classpath on “both sides”
  • no more implicit contract between “throwing side” and “catching side” in exception handling

I hope that all makes sense. Sorry if my wording or terminology are unclear; I’m still new to the Axon Framework. :slight_smile: However, I’ve enjoyed learning it so far, and I really appreciate the help from you and others on this forum!

Sure thing, @0xFFFF_FFFF! Glad to help. :slight_smile:
Let’s go through your questions one by one:


Answer 1

By using Axon Server, you effectively use a CommandBus with distributed capabilities, as it provides a seamless integration for message distribution automatically.
So, eventhough you may run a single instance, Axon Framework recognizes the connection with Axon Server, and thus starts an AxonServerCommandBus instead.

If you would disable Axon Server in your sample project, Axon Framework will revert to a local CommandBus.
If you want to try this out, you can set the axon.axonserver.enabled flag to false in a properties file.

Answer 2

To tell you the truth, this is actually how it used to work in the past.
When we introduced Axon Server, we had a rise in issues asking why a ClassNotFoundException was thrown.

Although we could’ve left it at that, we felt that plainly assuming the thrown exception by the handler to be part of the common API of all dispatching services to be incorrect.
It should, instead, be a conscious decision what the returned result is, as it’s in essence part of the contract.

That is what I was aiming for by stating “the concrete type of exception is a result that’s not accounted for when dispatching”.
The API of a dispatch operation does not clarify what result the dispatcher can deal with.

Note that this does not say that there’s no common grounds to construct a configuration means to allow exceptions to be serialized and deserialized as you’re expecting.
It’s simply not in place at this stage, nor am I certain whether the API’s allow us to support this in a backwards compatible fashion.
So, food for though.

Answer 3

I don’t agree that adding the UsernameAlreadyTakenException on the classpath of both (or more) applications is more concrete than the details object.
That’s just another form of an implicit contract, by assuming any service holds the exceptional class.

We may resolve the implicit contract by letting the handler registration define what results may occur.
Henceforth, allowing for a more explicit contract.

But, would you want any exception thrown by the handler to be clear to the message bus in question?
For some I would say yes, but definitely not for.
And, bottom line, the message bus needs to support serialization and distribution of the response in a useful format.

Changing the handler registration process is, sadly, not something we can do for Axon Framework 4 as it introduced breaking changes.
Allowing a form of exception mapper (as I do in the recording) would be more straightforward to add to the, for example, CommandGateway within AF 4’s lifecycle.

No offence, but I also think the confusion comes from the fact it’s unclear such a message-driven paradigms leads to a distributed system.
Whenever communication flows from one instance to another over REST an exception mapping is also taking place.
Why should that be any different when using Commands, Events, and Queries?

Although, yes, you are not alone, my gut tells me there’s a certain level getting used to this paradigm taking effect.


I hope this clarifies things further, @0xFFFF_FFFF.
And, I am happy to hear you’re enjoying the journey so far. :slight_smile:

As stated in my previous reply, be sure to reach out of you have any remarks on the above.

1 Like