Exception mappers
Checking "does the post exist" in every resource method is tedious, and a 404 response without a body tells the client little. It is more convenient to throw your own exception deep in the code and turn it into an HTTP response in one place. Jakarta REST has the ExceptionMapper for this — a class that receives an exception and returns a Response.
Your own exception
Create the file src/main/java/com/example/blog/PostNotFoundException.java:
// src/main/java/com/example/blog/PostNotFoundException.java
package com.example.blog;
public class PostNotFoundException extends RuntimeException {
public PostNotFoundException(long id) {
super("Post " + id + " not found");
}
}And a record for the error response body:
// src/main/java/com/example/blog/ErrorMessage.java
package com.example.blog;
public record ErrorMessage(int status, String error) {
}The mapper
// src/main/java/com/example/blog/PostNotFoundMapper.java
package com.example.blog;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
@Provider
public class PostNotFoundMapper implements ExceptionMapper<PostNotFoundException> {
@Override
public Response toResponse(PostNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.type(MediaType.APPLICATION_JSON)
.entity(new ErrorMessage(404, e.getMessage()))
.build();
}
}The @Provider annotation registers the class with Jakarta REST. The type in angle brackets, ExceptionMapper<PostNotFoundException>, says which exceptions it handles. When a resource method throws such an exception, the server calls toResponse and sends its result.
The store throws the exception
Add a get method to PostStore and change delete:
// src/main/java/com/example/blog/PostStore.java (fragment)
public Post get(long id) {
return findById(id).orElseThrow(() -> new PostNotFoundException(id));
}
public void delete(long id) {
if (posts.remove(id) == null) {
throw new PostNotFoundException(id);
}
}The resource methods become short — no more checks in them:
// src/main/java/com/example/blog/PostResource.java (fragment)
@GET
@Path("/{id}")
public Post show(@PathParam("id") long id) {
return store.get(id);
}
@DELETE
@Path("/{id}")
public void delete(@PathParam("id") long id) {
store.delete(id);
}Trying it out
curl -i http://localhost:8080/blog/api/posts/99HTTP/1.1 404 Not Found
Content-Type: application/json
Content-Length: 42
{"error":"Post 99 not found","status":404}
Deleting a missing post (curl -i -X DELETE http://localhost:8080/blog/api/posts/42) returns the same response with the number 42.
Which mapper is chosen
| Situation | What happens |
|---|---|
| There is a mapper for this exception class | It is called |
There is only a mapper for a parent, for example RuntimeException | The parent's mapper is called |
| No mappers | Status 500, the exception goes to the log |
A mapper for Exception catches everything — it is sometimes added to hide internal messages from clients. But then it also intercepts the NotFoundException from the previous lesson, so inside it you have to handle WebApplicationException separately.
Common mistake
Forgetting the @Provider annotation. The class compiles, but the server does not know about it, and the exception reaches the client as a 500 error with the message text:
HTTP/1.1 500 Internal Server Error
Post 99 not found
The server log shows RESTEASY002375: Error processing request GET /blog/api/posts/99 and the PostNotFoundException stack trace.