Error handling
When something goes wrong, the client should get the right status code and a clear message, not a 500 "Internal Server Error". Micronaut gives you three ways to report an error: return an empty result, throw a ready-made exception with a code, or throw your own exception and write a handler for it.
An empty result
You have already seen the first way: the method returns an Optional, and when it is empty, Micronaut answers 404 by itself. The same happens when the method returns null. This is handy, but the error text is generic: "Page Not Found".
An exception with a code
HttpStatusException turns into a response with the given code and your message. Suppose the first, welcome article must not be deleted:
// src/main/java/com/example/ArticleController.java
@Delete("/{id}")
public HttpResponse<Void> delete(Long id) {
if (id == 1) {
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The first article cannot be deleted");
}
return HttpResponse.noContent();
}The classes are imported from io.micronaut.http.exceptions and io.micronaut.http. The request curl -X DELETE http://localhost:8080/articles/1 returns 403:
{"_links":{"self":[{"href":"/articles/1","templated":false}]},"_embedded":{"errors":[{"message":"The first article cannot be deleted"}]},"message":"Forbidden"}Your own exception
An exception from the HTTP package should not be thrown in a service: a service should not know about HTTP. A domain exception is better:
// src/main/java/com/example/ArticleNotFoundException.java
package com.example;
public class ArticleNotFoundException extends RuntimeException {
public ArticleNotFoundException(Long id) {
super("Article " + id + " not found");
}
}The controller throws it when there is no such article:
// src/main/java/com/example/ArticleController.java
@Get("/{id}")
public Article show(Long id) {
return articleService.findById(id)
.orElseThrow(() -> new ArticleNotFoundException(id));
}An exception handler
A handler is a bean that implements ExceptionHandler and turns an exception into a response. We describe the response body with a short record:
// src/main/java/com/example/ErrorMessage.java
package com.example;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
public record ErrorMessage(String error) {
}// src/main/java/com/example/ArticleNotFoundHandler.java
package com.example;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.ExceptionHandler;
import jakarta.inject.Singleton;
@Produces
@Singleton
public class ArticleNotFoundHandler
implements ExceptionHandler<ArticleNotFoundException, HttpResponse<ErrorMessage>> {
@Override
public HttpResponse<ErrorMessage> handle(HttpRequest request, ArticleNotFoundException e) {
return HttpResponse.notFound(new ErrorMessage(e.getMessage()));
}
}The first type parameter is the exception we catch, the second is what we return. @Produces without arguments means a JSON response. The handler works for every controller.
curl -i http://localhost:8080/articles/42HTTP/1.1 404 Not Found
content-type: application/json
{"error":"Article 42 not found"}Which way to choose
| Way | When it fits |
|---|---|
Optional or null | A simple "not found" in a controller |
HttpStatusException | A one-off error right in a controller |
Your own exception and an ExceptionHandler | An error from a service, the same response across the whole API |
Common mistake
If you forget @Singleton on the handler, Micronaut does not see it: the exception reaches the framework, and the client gets 500. The log contains only the exception text:
ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: Article 42 not foundA handler is an ordinary bean and needs a scope annotation.