Handling Exceptions in JAX-RS Jersey with ExceptionMapper

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:

  1. Implement the ExceptionMapper<T> interface, where T is the type of exception you want to handle.
  2. Override the toResponse(T exception) method.
  3. Annotate your implementation with @Provider so 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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.