Exception handling is a critical aspect of building robust REST APIs. In Quarkus, you can implement global exception handling to consistently manage errors across your application. This guide covers various approaches with complete examples.
1. Using @Provider
with ExceptionMapper
The most common approach is to implement JAX-RS ExceptionMapper
interfaces.
Example: Basic Exception Mapper
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class GlobalExceptionHandler implements ExceptionMapper<Exception> {
@Override
public Response toResponse(Exception exception) {
ErrorResponse errorResponse = new ErrorResponse(
"SERVER_ERROR",
exception.getMessage(),
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()
);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(errorResponse)
.build();
}
}
// Error response DTO
public class ErrorResponse {
private String code;
private String message;
private int status;
// Constructors, getters, setters
public ErrorResponse(String code, String message, int status) {
this.code = code;
this.message = message;
this.status = status;
}
// Getters and setters...
}
2. Handling Specific Exceptions
You can create mappers for specific exception types.
Example: Custom Business Exception
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
@Provider
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {
@Override
public Response toResponse(BusinessException exception) {
ErrorResponse errorResponse = new ErrorResponse(
"BUSINESS_ERROR",
exception.getMessage(),
Response.Status.BAD_REQUEST.getStatusCode()
);
return Response.status(Response.Status.BAD_REQUEST)
.entity(errorResponse)
.build();
}
}
3. Validation Exception Handling
For handling bean validation errors (Hibernate Validator):
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.Response;
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException exception) {
List<Violation> violations = exception.getConstraintViolations()
.stream()
.map(this::toViolation)
.collect(Collectors.toList());
ErrorResponse errorResponse = new ErrorResponse(
"VALIDATION_ERROR",
"Validation failed",
Response.Status.BAD_REQUEST.getStatusCode(),
violations
);
return Response.status(Response.Status.BAD_REQUEST)
.entity(errorResponse)
.build();
}
private Violation toViolation(ConstraintViolation<?> violation) {
return new Violation(
violation.getPropertyPath().toString(),
violation.getMessage()
);
}
}
// Extended ErrorResponse with violations
public class ErrorResponse {
private String code;
private String message;
private int status;
private List<Violation> violations;
// Constructors, getters, setters...
}
public class Violation {
private String field;
private String message;
// Constructors, getters, setters...
}
4. Using Quarkus Reactive Routes Exception Handling
For reactive routes, you can use:
import io.quarkus.vertx.web.Route;
import io.vertx.ext.web.RoutingContext;
public class ExceptionHandlingRoutes {
@Route(path = "/api/items", methods = HttpMethod.GET)
void getItems(RoutingContext rc) {
try {
// Your business logic
rc.response().end("Success");
} catch (Exception e) {
handleException(rc, e);
}
}
private void handleException(RoutingContext rc, Throwable throwable) {
if (throwable instanceof BusinessException) {
rc.response()
.setStatusCode(400)
.end(new ErrorResponse("BUSINESS_ERROR", throwable.getMessage(), 400).toString());
} else {
rc.response()
.setStatusCode(500)
.end(new ErrorResponse("SERVER_ERROR", "Internal server error", 500).toString());
}
}
}
5. Using CDI Interceptors
You can create a CDI interceptor for exception handling:
import javax.annotation.Priority;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
@ErrorHandled
public class ErrorHandlingInterceptor {
@AroundInvoke
public Object handleErrors(InvocationContext context) throws Exception {
try {
return context.proceed();
} catch (BusinessException e) {
throw new WebApplicationException(
Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("BUSINESS_ERROR", e.getMessage(), 400))
.build()
);
} catch (Exception e) {
throw new WebApplicationException(
Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("SERVER_ERROR", e.getMessage(), 500))
.build()
);
}
}
}
// Annotation to mark methods for interception
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface ErrorHandled {
}
// Usage in your resource
@Path("/api")
public class MyResource {
@GET
@Path("/data")
@ErrorHandled
public Response getData() {
// Your code that might throw exceptions
}
}
6. Handling Asynchronous Exceptions
For async endpoints, you need to handle exceptions differently:
import java.util.concurrent.CompletionException;
@Provider
public class AsyncExceptionMapper implements ExceptionMapper<CompletionException> {
@Override
public Response toResponse(CompletionException exception) {
Throwable cause = exception.getCause();
if (cause instanceof BusinessException) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("BUSINESS_ERROR", cause.getMessage(), 400))
.build();
}
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("SERVER_ERROR", "Internal server error", 500))
.build();
}
}
7. Logging Exceptions
It's important to log exceptions while handling them:
import org.jboss.logging.Logger;
@Provider
public class GlobalExceptionHandler implements ExceptionMapper<Exception> {
private static final Logger LOG = Logger.getLogger(GlobalExceptionHandler.class);
@Override
public Response toResponse(Exception exception) {
LOG.error("Unhandled exception occurred", exception);
ErrorResponse errorResponse = new ErrorResponse(
"SERVER_ERROR",
"Internal server error", // Don't expose internal details to clients
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()
);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(errorResponse)
.build();
}
}
8. Complete Example Application
Here's a complete example with all components:
1. Exception Classes
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
2. DTOs
public class ErrorResponse {
private String code;
private String message;
private int status;
private List<Violation> violations;
// Constructors, getters, setters
}
public class Violation {
private String field;
private String message;
// Constructors, getters, setters
}
3. Exception Mappers
@Provider
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {
@Override
public Response toResponse(BusinessException exception) {
ErrorResponse errorResponse = new ErrorResponse(
"BUSINESS_ERROR",
exception.getMessage(),
Response.Status.BAD_REQUEST.getStatusCode()
);
return Response.status(Response.Status.BAD_REQUEST)
.entity(errorResponse)
.build();
}
}
@Provider
public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
@Override
public Response toResponse(NotFoundException exception) {
ErrorResponse errorResponse = new ErrorResponse(
"NOT_FOUND",
exception.getMessage(),
Response.Status.NOT_FOUND.getStatusCode()
);
return Response.status(Response.Status.NOT_FOUND)
.entity(errorResponse)
.build();
}
}
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException exception) {
List<Violation> violations = exception.getConstraintViolations()
.stream()
.map(this::toViolation)
.collect(Collectors.toList());
ErrorResponse errorResponse = new ErrorResponse(
"VALIDATION_ERROR",
"Validation failed",
Response.Status.BAD_REQUEST.getStatusCode(),
violations
);
return Response.status(Response.Status.BAD_REQUEST)
.entity(errorResponse)
.build();
}
private Violation toViolation(ConstraintViolation<?> violation) {
return new Violation(
violation.getPropertyPath().toString(),
violation.getMessage()
);
}
}
@Provider
public class GlobalExceptionHandler implements ExceptionMapper<Exception> {
private static final Logger LOG = Logger.getLogger(GlobalExceptionHandler.class);
@Override
public Response toResponse(Exception exception) {
LOG.error("Unhandled exception occurred", exception);
ErrorResponse errorResponse = new ErrorResponse(
"SERVER_ERROR",
"Internal server error",
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()
);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(errorResponse)
.build();
}
}
4. Resource Example
@Path("/api/items")
public class ItemResource {
@GET
@Path("/{id}")
public Response getItem(@PathParam("id") Long id) {
if (id == 0) {
throw new NotFoundException("Item not found");
}
if (id < 0) {
throw new BusinessException("Invalid item ID");
}
return Response.ok().entity(new Item(id, "Sample Item")).build();
}
@POST
@ValidateRequest
public Response createItem(@Valid Item item) {
// Your business logic
return Response.status(Response.Status.CREATED).entity(item).build();
}
}
// Item class with validation
public class Item {
@NotNull
private Long id;
@NotBlank
@Size(min = 3, max = 50)
private String name;
// Constructors, getters, setters
}
Best Practices
- Don't expose stack traces: In production, avoid exposing detailed error messages or stack traces to clients.
- Use appropriate HTTP status codes: Match the status code to the error type.
- Log exceptions: Always log exceptions for debugging purposes.
- Standardize error responses: Use a consistent error response format across your API.
- Handle validation separately: Provide detailed validation error messages.
- Consider security: Ensure error messages don't reveal sensitive information.
This comprehensive approach to exception handling will make your Quarkus REST API more robust and maintainable while providing consistent error responses to clients.