1. Introduction

In this tutorial, we’re going to see different ways of handling exceptions with Jersey, which is a JAX-RS implementation.

JAX-RS gives us many mechanisms to handle exceptions, that we can choose and combine. Handling REST exceptions is an important step to build a better API. In our use case, we’ll build an API for buying stocks and see how each step affects the other.

2. Scenario Setup

Our minimal setup involves creating a repository, a couple of beans, and some endpoints. It starts with our resource configuration. There, we’ll define our starting URL with @ApplicationPath and our endpoints package:

@ApplicationPath("/exception-handling/*")
public class ExceptionHandlingConfig extends ResourceConfig {
    public ExceptionHandlingConfig() {
        packages("com.baeldung.jersey.exceptionhandling.rest");
    }
}

2.1. Beans

We’ll need only two beans: Stock and Wallet, so we can save Stocks and buy them. For our Stock, we just need a price property to help with validations. More importantly, our Wallet class will have validation methods to help build our scenario:

public class Wallet {
    private String id;
    private Double balance = 0.0;

    // getters and setters

    public Double addBalance(Double amount) {
        return balance += amount;
    }

    public boolean hasFunds(Double amount) {
        return (balance - amount) >= 0;
    }
}

2.2. Endpoints

Similarly, our API will have two endpoints. These will define standard methods to save and retrieve our beans:

@Path("/stocks")
public class StocksResource {
    // POST and GET methods
}
@Path("/wallets")
public class WalletsResource {
    // POST and GET methods
}

For example, let’s see our GET method in StocksResource:

@GET
@Path("/{ticker}")
@Produces(MediaType.APPLICATION_JSON)
public Response get(@PathParam("ticker") String id) {
    Optional<Stock> stock = stocksRepository.findById(id);
    stock.orElseThrow(() -> new IllegalArgumentException("ticker"));

    return Response.ok(stock.get())
      .build();
}

In our GET method, we’re throwing our first exception. We’ll only handle that later, so we can see its effects.

3. What Happens When We Throw an Exception?

When an unhandled exception occurs, we might expose sensitive information about the internals of our application. If we try that GET method from StocksResource with a nonexistent Stock, we get a page similar to this:

default exception screen

This page shows the application server and version, which might help potential attackers to exploit vulnerabilities. Also, there is information regarding our class names and line numbers, which might also help attackers. Most importantly, most of this information is useless to API users and gives a bad impression.

To help control exceptional responses, JAX-RS provides the classes ExceptionMapper and WebApplicationException. Let’s see how they work.

4. Custom Exceptions With WebApplicationException

With WebApplicationException, we can create custom exceptions. This special type of RuntimeException lets us define a response status and entity. We’ll start by creating an InvalidTradeException that sets a message and a status:

public class InvalidTradeException extends WebApplicationException {
    public InvalidTradeException() {
        super("invalid trade operation", Response.Status.NOT_ACCEPTABLE);
    }
}

Also worthy of mention, JAX-RS defines subclasses of WebApplicationException for common HTTP status codes. These include useful exceptions like NotAllowedException, BadRequestException, etc. But, when we want more complex error messages, we can return a JSON response.

4.1. JSON Exceptions

We can create simple Java classes and include them in our Response. In our example, we have a subject property, which we’ll use to wrap contextual data:

public class RestErrorResponse {
    private Object subject;
    private String message;

    // getters and setters
}

Since this exception is not meant to be manipulated, we won’t worry about the type of subject.

4.2. Putting Everything to Use

To see how we can use custom exceptions, let’s define a method for buying a Stock:

@POST
@Path("/{wallet}/buy/{ticker}")
@Produces(MediaType.APPLICATION_JSON)
public Response postBuyStock(
  @PathParam("wallet") String walletId, @PathParam("ticker") String id) {
    Optional<Stock> stock = stocksRepository.findById(id);
    stock.orElseThrow(InvalidTradeException::new);

    Optional<Wallet> w = walletsRepository.findById(walletId);
    w.orElseThrow(InvalidTradeException::new);

    Wallet wallet = w.get();
    Double price = stock.get()
      .getPrice();

    if (!wallet.hasFunds(price)) {
        RestErrorResponse response = new RestErrorResponse();
        response.setSubject(wallet);
        response.setMessage("insufficient balance");
        throw new WebApplicationException(Response.status(Status.NOT_ACCEPTABLE)
          .entity(response)
          .build());
    }

    wallet.addBalance(-price);
    walletsRepository.save(wallet);

    return Response.ok(wallet)
      .build();
}

In this method, we use everything we’ve created so far. We throw an InvalidTradeException for nonexistent stocks or wallets. And, if we have insufficient funds, build a RestErrorResponse containing our Wallet, and throw it as a WebApplicationException.

4.3. Use Case Example

Firstly, let’s create a Stock:

$ curl 'http://localhost:8080/jersey/exception-handling/stocks' -H 'Content-Type: application/json' -d '{
    "id": "STOCK",
    "price": 51.57
}'

{"id": "STOCK", "price": 51.57}

Then a Wallet to buy it:

$ curl 'http://localhost:8080/jersey/exception-handling/wallets' -H 'Content-Type: application/json' -d '{
    "id": "WALLET",
    "balance": 100.0
}'

{"balance": 100.0, "id": "WALLET"}

After that, we’ll buy the Stock using our Wallet:

$ curl -X POST 'http://localhost:8080/jersey/exception-handling/wallets/WALLET/buy/STOCK'

{"balance": 48.43, "id": "WALLET"}

And we’ll get our updated balance in the response. Also, if we try to buy again, we’ll get our detailed RestErrorResponse:

{
    "message": "insufficient balance",
    "subject": {
        "balance": 48.43,
        "id": "WALLET"
    }
}

5. Unhandled Exceptions With ExceptionMapper

To clarify, throwing a WebApplicationException won’t be enough to get rid of the default error page. We have to specify an entity for our Response, which is not the case for InvalidTradeException. Often, as much as we try to handle all scenarios, an unhandled exception still might occur. So it’s a good idea to start by handling those. With ExceptionMapper, we define catch points for specific types of exceptions, and modify the Response before committing it:

public class ServerExceptionMapper implements ExceptionMapper<WebApplicationException> {
    @Override
    public Response toResponse(WebApplicationException exception) {
        String message = exception.getMessage();
        Response response = exception.getResponse();
        Status status = response.getStatusInfo().toEnum();

        return Response.status(status)
          .entity(status + ": " + message)
          .type(MediaType.TEXT_PLAIN)
          .build();
    }
}

For example, we’re just repassing exception information into our Response, which will display exactly what we return. Subsequently, we can go a little further by checking the status code before building our Response:

switch (status) {
    case METHOD_NOT_ALLOWED:
        message = "HTTP METHOD NOT ALLOWED";
        break;
    case INTERNAL_SERVER_ERROR:
        message = "internal validation - " + exception;
        break;
    default:
        message = "[unhandled response code] " + exception;
}

5.1. Handling Specific Exceptions

If there’s a specific Exception that is thrown often, we can also create an ExceptionMapper for it. In our endpoints, we throw an IllegalArgumentException for simple validations, so let’s start with a mapper for it. This time, with a JSON response:

public class IllegalArgumentExceptionMapper
  implements ExceptionMapper<IllegalArgumentException> {
    @Override
    public Response toResponse(IllegalArgumentException exception) {
        return Response.status(Response.Status.EXPECTATION_FAILED)
          .entity(build(exception.getMessage()))
          .type(MediaType.APPLICATION_JSON)
          .build();
    }

    private RestErrorResponse build(String message) {
        RestErrorResponse response = new RestErrorResponse();
        response.setMessage("an illegal argument was provided: " + message);
        return response;
    }
}

Now every time an unhandled IllegalArgumentException occurs in our application, our IllegalArgumentExceptionMapper will handle it.

5.2. Configuration

To activate our exception mappers, we have to go back to our Jersey resource configuration and register them:

public ExceptionHandlingConfig() {
    // packages ...
    register(IllegalArgumentExceptionMapper.class);
    register(ServerExceptionMapper.class);
}

This is enough to get rid of the default error page. Then, depending on what is thrown, Jersey will use one of our exception mappers when an unhandled exception occurs. For instance, when trying to get a Stock that doesn’t exist, the IllegalArgumentExceptionMapper will be used:

$ curl 'http://localhost:8080/jersey/exception-handling/stocks/NONEXISTENT'

{"message": "an illegal argument was provided: ticker"}

Likewise, for other unhandled exceptions, the broader ServerExceptionMapper will be used. For example, when we use the wrong HTTP method:

$ curl -X POST 'http://localhost:8080/jersey/exception-handling/stocks/STOCK'

Method Not Allowed: HTTP 405 Method Not Allowed

6. Conclusion

In this article, we saw the many ways we can handle exceptions using Jersey. Moreover, why it’s important, and how to configure it. After that, we built a simple scenario where we could apply them. As a result, we now have a friendlier and more secure API.

And as always, the source code is available over on GitHub.