Micronaut Error Handling
Error handling is one of the main concerns when developing systems. On a code level, error handling handles exceptions thrown by the code we write. On a service level, by errors, we mean all the non-successful responses we return. It’s a good practice in large systems, to handle similar errors consistently. For example, in a service with two controllers, we want the authentication error responses to be similar, so that we can debug issues easier. Taking a step back, we probably want the same error response from all services of a system, for simplicity. We can implement this approach by using global exception handlers. In this tutorial, we’ll focus on the error handling in Micronaut. Similar to most of the Java frameworks, Micronaut provides a mechanism to handle errors commonly because error handling is important. We’ll discuss micronaut error handling and we’ll demonstrate it in examples.
1. Overview
Error handling in Micronaut is pivotal for building resilient applications. Micronaut offers several robust mechanisms to manage exceptions and ensure your application handles errors gracefully. One of the core tools is the @Error annotation, which allows developers to define custom error handlers within their controllers. This enables precise control over how specific exceptions and HTTP status codes are managed, ensuring consistent and predictable responses.
Furthermore, Micronaut supports global exception handling, capturing exceptions not caught at the controller level. This centralized approach ensures that even unexpected errors are managed uniformly across the application. By leveraging these features, developers can enhance their applications’ stability and user experience, making error handling not just a backend concern but a crucial aspect of overall application quality.
Now let us look at how to handle error and exception handling in Micronaut.
2. Getting Started on Error Handling in Micronaut
Error handling is a critical aspect of developing robust and reliable applications. Micronaut provides a range of features to effectively manage errors, ensuring that applications can gracefully handle exceptions and unexpected issues. This section will guide you through the essential steps for implementing error handling in Micronaut, helping you build resilient systems.
First, you need to set up Java 17. You can download Java and install it.
Then , You can download or set up the Gradle. The gradle command for setup is :
$ mkdir /opt/gradle $ unzip -d /opt/gradle gradle-8.10.2-bin.zip $ ls /opt/gradle/gradle-8.10.2
After setting up, you can run the gradle command to check the version. The gradle command is shown below:
gradle -v
The output of the executed command is shown below:
bhagvanarch@Bhagvans-MacBook-Air micronaut % gradle -v ------------------------------------------------------------ Gradle 8.10.2 ------------------------------------------------------------ Build time: 2024-09-23 21:28:39 UTC Revision: 415adb9e06a516c44b391edff552fd42139443f7 Kotlin: 1.9.24 Groovy: 3.0.22 Ant: Apache Ant(TM) version 1.10.14 compiled on August 16 2023 Launcher JVM: 23 (Homebrew 23) Daemon JVM: /opt/homebrew/Cellar/openjdk/23/libexec/openjdk.jdk/Contents/Home (no JDK specified, using current Java home) OS: Mac OS X 14.6.1 aarch64 bhagvanarch@Bhagvans-MacBook-Air micronaut %
Now you can create Application java file as shown below:
package org.javacodegeeks.micronaut; import io.micronaut.runtime.Micronaut; public class Application { public static void main(String[] args) { Micronaut.run(Application.class, args); } }
Then, you can create Customer Controller java class. The code is shown below:
package org.javacodegeeks.micronaut; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Produces; @Controller("/customer") public class CustomerController { @Get @Produces(MediaType.TEXT_PLAIN) public String index() { return "There are no customers yet"; } }
You can build the application using the command below:
./gradlew run
The output for the above commands when executed is shown below:
bhagvanarch@Bhagvans-MacBook-Air micronaut % ./gradlew run > Task :run __ __ _ _ | \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_ | |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __| | | | | | (__| | | (_) | | | | (_| | |_| | |_ |_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__| 19:21:59.389 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 261ms. Server Running: http://localhost:8080 80% EXECUTING [5m 47s] > :run
You can see the micronaut API running. The output for customer call is shown below:
Micronaut Example
2.1 Logging and Monitoring : Micronaut Error handling
Effective error handling also involves logging and monitoring to detect and resolve issues promptly. Micronaut integrates seamlessly with logging frameworks like Logback, allowing you to log information, errors, and exceptions for further analysis because the logs will help in root cause analysis.
Here’s an example of adding logging to the customer API:
package org.javacodegeeks.micronaut; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Produces; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Controller("/customer") public class CustomerController { private static final Logger LOG = LoggerFactory.getLogger(CustomerController.class); @Get @Produces(MediaType.TEXT_PLAIN) public String index() { LOG.info("in the customer api {}"); return "There are no customers yet"; } }
The output when built and executed will show the logging on the console as shown below:
bhagvanarch@Bhagvans-MacBook-Air micronaut % ./gradlew run > Task :run __ __ _ _ | \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_ | |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __| | | | | | (__| | | (_) | | | | (_| | |_| | |_ |_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__| 19:43:03.872 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 268ms. Server Running: http://localhost:8080 19:43:17.203 [default-nioEventLoopGroup-1-2] INFO o.j.micronaut.CustomerController - in the customer api {} 80% EXECUTING [3m 28s] > :run
In this example, informational logging for customer API is shown helping you track and investigate issues efficiently.
Now let us look at Error Handling in Micronaut using @Error annotation.
3. Error Handling in Micronaut – @Error Annotation
Error handling is a fundamental aspect of developing reliable applications. Micronaut, a modern JVM-based framework, offers a sophisticated way to handle errors using @Error
annotation. This annotation simplifies the process of managing exceptions and providing meaningful responses, ensuring a smooth user experience.
3.1 Setting Up Error Handlers Using the @Error
Annotation
The @Error
annotation allows you to define custom error handlers within your controllers. These handlers can manage specific exceptions or HTTP status codes, providing a tailored response. Here’s a basic example:
package org.javacodegeeks.micronaut; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Produces; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.annotation.Error; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Controller("/customer") public class CustomerController { private static final Logger LOG = LoggerFactory.getLogger(CustomerController.class); @Get @Produces(MediaType.TEXT_PLAIN) public String index() { LOG.info("in the customer api {}"); return "There are no customers yet"; } @Get("/examerror") public String errorEndpoint() { throw new RuntimeException("An unexpected error occurred"); } @Error(exception = RuntimeException.class) public HttpResponse handleRuntimeException(RuntimeException exception) { return HttpResponse.serverError(" The error message is: " + exception.getMessage()); } }
In this example, any RuntimeException
thrown within the CustomerController
is caught by the handleRuntimeException
method, which returns a custom error message “The error message is: An unexpected error occurred”, when called from the browser – http://localhost:8080/customer/examerror
3.2 Handling Specific HTTP Status Codes
Micronaut also allows you to handle specific HTTP status codes using the @Error
annotation. This is useful when you need to provide custom responses for different error conditions. Here’s how you can do it:
package org.javacodegeeks.micronaut; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.MediaType; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Error; import io.micronaut.http.hateoas.JsonError; import io.micronaut.http.hateoas.Link; import io.micronaut.views.ViewsRenderer; import java.util.Collections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Controller("/newresource") public class NewResourceController { private static final Logger LOG = LoggerFactory.getLogger(NewResourceController.class); private final ViewsRenderer viewsRenderer; public NewResourceController(ViewsRenderer viewsRenderer) { this.viewsRenderer = viewsRenderer; } @Error(status = HttpStatus.NOT_FOUND, global = true) public HttpResponse notFound(HttpRequest request) { LOG.error("there is no new resource"); if (request.getHeaders() .accept() .stream() .anyMatch(mediaType -> mediaType.getName().contains(MediaType.TEXT_HTML))) { return HttpResponse.ok(viewsRenderer.render("notFound", Collections.emptyMap(), request)) .contentType(MediaType.TEXT_HTML); } JsonError error = new JsonError("New Resource is not Found") .link(Link.SELF, Link.of(request.getUri())); return HttpResponse.notFound() .body(error); } }
In this example, Invoking /customer/newresource triggers the request that results in a 404 Not Found status will be handled by the notFound
method, returning a custom error message defined in the resource notFound.vm
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Not Found</title> </head> <body> <h1>NOT FOUND</h1> <p><b>The page you were looking is moved, deleted or does not exist.</b></p> <p>This is can be because:</p> <ul> <li>An outdated resource on another site</li> <li>A typing error in the address / URL</li> </ul> </body> </html>
Error handling in Micronaut using the @Error
annotation is a powerful and flexible way to manage exceptions and provide meaningful responses. By leveraging controller-specific and global error handlers, along with customizing error responses, you can ensure your application handles errors gracefully and maintains a positive user experience. This section has covered the essential steps for implementing effective error handling in Micronaut, equipping you with the tools to manage exceptions seamlessly.
4. Error Handling in Micronaut – using the Exception Handler
Handling errors effectively is a cornerstone of building resilient applications. Micronaut, a modern JVM-based framework, provides a structured way to manage errors using the ExceptionHandler
interface. This interface allows developers to create custom error-handling logic, ensuring that exceptions are caught and managed gracefully, enhancing the user experience and maintaining the stability of the application.
4.1 Creating Custom Exception Handlers
To utilize the ExceptionHandler
interface, you need to create a class that implements it. This class will define the logic for handling specific exceptions. Here’s a step-by-step guide:
First define the custom Exception:
package org.javacodegeeks.micronaut; public class CustomException extends RuntimeException { public CustomException(String message) { super(message); } }
Now Implement the ExceptionHandler Interface
Next, create a class that implements the ExceptionHandler
interface for your custom exception. This class will contain the logic for handling the exception and returning a meaningful response to the client:
package org.javacodegeeks.micronaut; @Singleton public class CustomExceptionHandler implements ExceptionHandler { @Override public HttpResponse handle(HttpRequest request, CustomException exception) { Map errorResponse = new HashMap(); errorResponse.put("message", exception.getMessage()); errorResponse.put("status", "error"); return HttpResponse.badRequest(errorResponse); }
4.2 Registering the Exception Handler
Micronaut automatically discovers and registers exception handlers that are annotated with @Singleton
. By default, Micronaut scans for these handlers at runtime and applies them to the corresponding exceptions.
4.2.1 Testing the Exception Handler
To see your custom exception handler in action, you can create a simple controller that throws the custom exception:
@Controller("/api") public class TestController { @Get("/test") public String testEndpoint() { throw new CustomException("This is a custom error message."); } }
When you access the /api/test
endpoint, the CustomException
will be thrown, and the CustomExceptionHandler
will catch it, returning the defined error response – This is a custom error message.
4.2.2 Enhancing Exception Handling Logic
You can further enhance your exception handler by adding more detailed error information, logging the exception, or even notifying external monitoring systems. Here’s an example with added logging:
package org.javacodegeeks.micronaut; @Singleton public class CustomExceptionHandler ExceptionHandler { private static final Logger LOG = LoggerFactory.getLogger(EnhancedExceptionHandler.class); @Override public HttpResponse handle(HttpRequest request, CustomException exception) { LOG.error("Handling custom exception: {}", exception.getMessage(), exception); Map errorResponse = new HashMap(); errorResponse.put("message", exception.getMessage()); errorResponse.put("status", "error"); return HttpResponse.badRequest(errorResponse); }
In this enhanced example, the handler logs the exception details before returning the error response, aiding in troubleshooting and monitoring.
Using the ExceptionHandler
interface in Micronaut provides a robust and flexible approach to managing errors. By creating custom exception handlers, you can ensure that your application handles errors in a consistent and meaningful way, enhancing both stability and user experience. This section has covered the essential steps for implementing error handling with the ExceptionHandler
interface, equipping you with the knowledge to manage exceptions effectively in your Micronaut applications.
4.3 Global Error Handling
While controller-specific error handling is effective, you may also want to handle exceptions at a global level. This ensures a consistent error handling strategy across your entire application. To create a global error handler, define a new class and annotate it with @ControllerAdvice:
@ControllerAdvice public class GlobalErrorHandler { @Error(global = true, exception = RuntimeException.class) public HttpResponse handleGlobalRuntimeException(RuntimeException e) { return HttpResponse.serverError("Global error message: " + e.getMessage()); } }
The GlobalErrorHandler class catches any RuntimeException not specifically handled within a controller, providing a standardized error response.Micronaut allows further customization of error responses to ensure they are informative and user-friendly. You can include additional details such as error codes, timestamps, and more:
@ControllerAdvice public class DetailedErrorHandler { @Error(global = true, exception = RuntimeException.class) public HttpResponse<Map> handleDetailedRuntimeException(RuntimeException e) { Map errorDetails = new HashMap(); errorDetails.put("message", e.getMessage()); errorDetails.put("timestamp", LocalDateTime.now()); errorDetails.put("status", HttpStatus.INTERNAL_SERVER_ERROR.getCode()); return HttpResponse.serverError(errorDetails); } }
In this example, the error response includes a detailed message, the current timestamp, and the HTTP status code, providing more context for debugging and user communication.
5.Conclusion
In summary, effective error handling in Micronaut is vital for developing resilient and user-friendly applications. By leveraging the powerful tools and annotations provided by the framework, such as @Error
and the ExceptionHandler
interface, developers can create custom and global error handlers tailored to their application’s needs. These mechanisms ensure exceptions are managed gracefully, providing informative and consistent responses to end-users.
Additionally, integrating logging and monitoring enhances the ability to track and address errors promptly, maintaining application stability. Ultimately, mastering error handling in Micronaut equips developers with the skills to build robust systems that can handle unexpected issues seamlessly, ensuring a positive user experience and reliable performance. This comprehensive approach to error management underscores the importance of thoughtful and proactive development practices in the Micronaut ecosystem.
6. Download
You can download the full source code of this example here: Error Handling in Micronaut