When building RESTful web services, proper exception handling is crucial. Unhandled exceptions can lead to ugly stack traces being sent to the client, exposing internal server details and providing a poor user experience. The JAX-RS specification provides an elegant solution for this: the ExceptionMapper.
In this tutorial, we’ll explore how to use Jersey’s implementation of ExceptionMapper to create centralized, custom, and consistent error responses for our API. We’ll build a simple Spring Boot application with Jersey to demonstrate the concepts.
What is an ExceptionMapper?
An ExceptionMapper is a JAX-RS component that “maps” a Java exception to a javax.ws.rs.core.Response object. When an exception is thrown from a JAX-RS resource method, the framework checks if there’s a registered ExceptionMapper for that specific exception type (or any of its superclasses).
If a mapper is found, its toResponse() method is called. This gives you complete control over the HTTP response sent back to the client, including:
- The HTTP Status Code (e.g., 404 Not Found, 400 Bad Request, 500 Internal Server Error)
- The Response Body (e.g., a structured JSON error object)
- Custom HTTP Headers
To create an exception mapper, you need to:
- Implement the
ExceptionMapper<T>interface, whereTis the type of exception you want to handle. - Override the
toResponse(T exception)method. - Annotate your implementation with
@Providerso the JAX-RS runtime can discover and register it.
Setting up a Spring Boot Project with Jersey
Let’s start by creating a new Spring Boot project. The easiest way to integrate Jersey is by using the spring-boot-starter-jersey starter. Add the following dependencies to your pom.xml:
<dependencies>
<!-- Spring Boot Starter for web services -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Jersey JAX-RS implementation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
</dependencies>
Step 1: Define the Models and Error Response POJO
First, let’s create a simple Employee model. We’ll also define a dedicated ErrorResponse class to ensure our API returns consistent, structured error messages in JSON format.
Employee.java
package com.ankurm.example.model;
public class Employee {
private int id;
private String name;
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
// Getters and Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
ErrorResponse.java
package com.ankurm.example.model;
public class ErrorResponse {
private String message;
private int status;
public ErrorResponse(String message, int status) {
this.message = message;
this.status = status;
}
// Getters and Setters
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
}
Step 2: Create a Custom Application Exception
It’s a good practice to create specific exceptions for your application’s business logic. Let’s create a RecordNotFoundException that we can throw when an employee is not found.
package com.ankurm.example.exception;
public class RecordNotFoundException extends RuntimeException {
public RecordNotFoundException(String message) {
super(message);
}
}
Step 3: Create a Specific ExceptionMapper
Now, let’s create the mapper for our custom RecordNotFoundException. This mapper will catch the exception and return a clean 404 Not Found response with our structured ErrorResponse object as the body.
package com.ankurm.example.exception;
import com.ankurm.example.model.ErrorResponse;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class RecordNotFoundExceptionMapper implements ExceptionMapper<RecordNotFoundException> {
@Override
public Response toResponse(RecordNotFoundException exception) {
ErrorResponse errorResponse = new ErrorResponse(
exception.getMessage(),
Response.Status.NOT_FOUND.getStatusCode()
);
return Response.status(Response.Status.NOT_FOUND)
.entity(errorResponse)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
Notice the @Provider annotation, which is essential for registration. Inside toResponse(), we build a Response object with a 404 status and our JSON error entity.
Step 4: Create a Generic “Catch-All” ExceptionMapper
What about other unexpected exceptions, like a NullPointerException? We should handle those too, to prevent stack traces from leaking. We can create a generic mapper that handles the base Throwable class. This will act as a fallback for any exception that doesn’t have a more specific mapper.
package com.ankurm.example.exception;
import com.ankurm.example.model.ErrorResponse;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
@Override
public Response toResponse(Throwable throwable) {
ErrorResponse errorResponse = new ErrorResponse(
"An internal server error occurred.",
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()
);
// Optionally log the exception
// e.g., using a logger: log.error("Unhandled exception caught", throwable);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(errorResponse)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
This mapper returns a generic 500 Internal Server Error. It’s a good place to add logging to record the details of unexpected errors for later analysis.
Step 5: Build the REST Resource
Let’s create a simple EmployeeResource that has a method to fetch an employee by ID. If the ID doesn’t exist, it will throw our RecordNotFoundException.
package com.ankurm.example.resource;
import com.ankurm.example.exception.RecordNotFoundException;
import com.ankurm.example.model.Employee;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Path("/employees")
public class EmployeeResource {
private static final Map<Integer, Employee> employees = new ConcurrentHashMap<>();
static {
employees.put(1, new Employee(1, "Ankur"));
employees.put(2, new Employee(2, "Mishra"));
}
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Employee getEmployeeById(@PathParam("id") int id) {
if (id == 99) {
// Simulate an unexpected error
throw new NullPointerException("A simulated unexpected error occurred.");
}
Employee employee = employees.get(id);
if (employee == null) {
throw new RecordNotFoundException("Employee not found with id: " + id);
}
return employee;
}
}
Step 6: Configure Jersey and Register Components
With Spring Boot and the Jersey starter, we need to create a configuration class that extends ResourceConfig. This is where we will register our resource class and our exception mappers.
package com.ankurm.example.config;
import com.ankurm.example.exception.GenericExceptionMapper;
import com.ankurm.example.exception.RecordNotFoundExceptionMapper;
import com.ankurm.example.resource.EmployeeResource;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@Component
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
// Register REST resources
register(EmployeeResource.class);
// Register ExceptionMappers
register(RecordNotFoundExceptionMapper.class);
register(GenericExceptionMapper.class);
}
}
By annotating this class with @Component, Spring Boot will automatically pick it up and configure Jersey.
Step 7: The Main Application Class
The final piece is our main Spring Boot application class, which is standard.
package com.ankurm.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JerseyExceptionMapperApplication {
public static void main(String[] args) {
SpringApplication.run(JerseyExceptionMapperApplication.class, args);
}
}
Testing the Application
Now, run the Spring Boot application. It will start a web server on port 8080 by default. You can use curl or any API client to test the endpoints.
1. Success Case: Fetching an Existing Employee
Request an employee that exists (e.g., ID 1).
curl http://localhost:8080/employees/1
You’ll get a successful 200 OK response with the employee data:
{
"id": 1,
"name": "Ankur"
}
2. Specific Exception Case: Employee Not Found
Request an employee that does not exist (e.g., ID 100). This will trigger our RecordNotFoundExceptionMapper.
curl -i http://localhost:8080/employees/100
The response will be a 404 Not Found with our custom error body:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"message": "Employee not found with id: 100",
"status": 404
}
3. Generic Exception Case: Unexpected Error
Request the employee with ID 99 to simulate an unexpected NullPointerException. This will be caught by our GenericExceptionMapper.
curl -i http://localhost:8080/employees/99
The response will be a 500 Internal Server Error with the generic error message:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"message": "An internal server error occurred.",
"status": 500
}
Conclusion
Using JAX-RS ExceptionMapper is a powerful and clean way to handle exceptions in your REST APIs. It allows you to decouple error handling logic from your business logic, avoid exposing server internals, and provide a consistent and user-friendly error response format for your API consumers. By creating both specific and generic mappers, you can ensure your application behaves predictably and gracefully, even when things go wrong.