So you're developing a REST API with Spring Boot and now you need to know how to handle errors.

Good news: I'll show you how to do that in this guide.

This is normally the point where I say "It's easier than you think," but truth be told if you want to handle errors properly, it's going to require some up-front planning and extra development.

But once you've got the framework in place, you can start handling different errors with ease.

Let's get started.

When a Status Code Isn't Enough

After reading those last few sentences you might be thinking to yourself: "Well if it takes that much extra work, I'll probably just send back an HTTP status code that describes the problem."

And you certainly could go that route. But is that really enough?

Let's say your REST API got a request that's unauthorized. In that case, you'd just send back a 401 (Unauthorized) and leave it at that.

But that status code doesn't explain why the request is unauthorized.

Were the credentials invalid? Was the JSON token missing? Was it expired? Did it have an invalid signature?

There are plenty of reasons why a request could be considered unauthorized. If your application just sends back a 401 with no explanation that doesn't offer a whole lot of helpful advice.

Similarly, an invalid request (400) status code by itself just tells the client that the request was invalid. But it doesn't say why.

In short: you need to send back more than just a status code.

On the Importance of a Response Status

When it comes to sending a response from your API, it's a great idea to not just send the response itself, but to send a response status along with it.

Why? See above. You need to give your clients as much info as possible. That's how the user will know how to take corrective action if there's a problem.

And please note: the response status is not the same as the HTTP status code.

The response status includes a machine-readable code (like "SUCCESS") and some human-readable text (like "Registration successful!").

The HTTP status code is a 3-digit number like 200 (OK), 400 (Bad Request), or 500 (Internal Server Error).

And yes, you need both the response status and the HTTP status code.

Here's why: in the event of an unauthorized action (HTTP status code 401), you can send back a response status with a code (like "BAD_CREDENTIALS") and some explanatory text (like "Invalid username or password").

So to emphasize the recurring theme here: the HTTP status code by itself isn't good enough.

Now let's take a look at some examples. Here's an example of a successful response:

    "responseStatus": {
        "statusCode": "OK",
        "message": "Successfully registered!"
    "response": {
        "id": "6164c4391160726f07cc3828",
        "username": "george",
        "firstName": "brian",
        "lastName": "carey",
        "email": "",
        "phoneNumber": "919-555-1212",

And here's an example of an error response:

    "responseStatus": {
        "statusCode": "ERROR",
        "message": "Validation error"
    "response": [
            "field": "lastName",
            "defaultMessage": "must not be null"

Okay. Now that you know the requirements, it's time to start putting some code together.

The Use Case

The use case for this API (and this guide) is to create an endpoint that handles new user registration. 

Users who register will need to provide info in mandatory fields: first name, last name, username, phone number, password, and email address.

If the user doesn't provide valid info, the API sends back an error response with an HTTP status of 400 (Bad Request). The response body will explain which fields are invalid and why.

Here's the model class that represents the registration form:

public class Registrant {

    @Size(min=1, max=32, message="First name must be between 1 and 32 characters")
    private String firstName;
    @Size(min=1, max=32, message="Last name must be between 1 and 32 characters")
    private String lastName;
    @Size(min=5, max=12, message="Username must be between 5 and 12 characters")
    private String username;
    @Size(min=8, max=20, message="Password must be between 8 and 20 characters")
    private String password;
    @Pattern(regexp = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$", message="Email address is invalid")
    private String emailAddress;

    private String phone;

//getters and setters


The annotations handle the validations. If you're unfamiliar with how all of that works, feel free to read all about validation in Spring Boot and then come on back here.

Now that you've got the model in place, it's time to write the code that handles responses.


First up is ResponseStatusCode. It's an enum that looks like this:

public enum ResponseStatusCode {

Only three (3) status codes to keep things simple. But you're free to use as many as you want.

You can also create various types of status codes. You might use numerical codes, for example. Or you might name the status codes after the members of your favorite K-Pop band.

It's up to you.

The important thing is: you need to let the client developers know what each status code means so they can act accordingly.


The ResponseStatus class marries a status code to a message.

public class ResponseStatus {
    private ResponseStatusCode statusCode;
    private String message;
    public ResponseStatusCode getStatusCode() {
        return statusCode;
    public void setStatusCode(ResponseStatusCode statusCode) {
        this.statusCode = statusCode;
    public String getMessage() {
        return message;
    public void setMessage(String message) {
        this.message = message;

That's pretty straightforward. And it represents the "responseStatus" field you saw in the example above.


Now you need to create a class that includes the three (3) important parts of every response:

  • The response status
  • The response itself
  • The HTTP status code

Remember: the response status is not the same as the HTTP status code. See above.

Start by creating an interface that defines the methods returning those three (3) pieces of info:

public interface IRestResponse<T> {

    ResponseStatus getResponseStatus();
    T getResponse();
    int getHttpStatusCode(); 

There ain't nothing too complicated about that.

But note that it's using a generic (the <T> next to the interface name). What's that all about?

Go back to the two sample responses above and take a look at the "response" fields. You'll note that they're different.

And that makes sense because the response could be anything. It might be a list of field errors or it might be details about a recently created user.

Because the response could be anything, it's a great idea to use a generic when representing it in code so the compiler will complain if you're inconsistent with your type references.

Now here's a concrete implementation of that interface:

public class RestResponse<T> implements IRestResponse<T> {

    private ResponseStatus responseStatus;
    private T response;
    private int httpStatusCode;

    public int getHttpStatusCode() {
        return httpStatusCode;
    public void setHttpStatusCode(int httpStatusCode) {
        this.httpStatusCode = httpStatusCode;
    public T getResponse() {
        return response;
    public void setResponse(T response) {
        this.response = response;
    public ResponseStatus getResponseStatus() {
        return responseStatus;
    public void setResponseStatus(ResponseStatus responseStatus) {
        this.responseStatus = responseStatus;
    public String toString() {
        return ReflectionToStringBuilder.toString(this);

I've chosen to create just one implementation, but you might choose to do more than one. For example, you might have one implementation for error responses and another for success responses.

By the way: note the @JsonIgnore on top of httpStatusCode. You should include that so that the response body doesn't show the HTTP status code. It doesn't need to display the status code because it gets sent back with the response anyway.


The ValidationError class lives up to its name by representing a field validation error like the one you saw in the sample response above.

public class ValidationError {

    private String field;
    private String code;
    private String defaultMessage;

//getters and setters


I'm not using the code property here but you're welcome to use it if you need it.

The field property holds the name of the field with the validation error and the defaultMessage property holds the explanatory, human-readable text.


The ValidationUtil class includes convenience methods related to valdiation. Here's a method that converts Spring's BindingResult object to a List of ValidationError objects.

public class ValidationUtil {
    public static List<ValidationError> convertBindingResultToValidationErrors(BindingResult bindingResult) {
        List<ValidationError> errors = new ArrayList<>();
        if (bindingResult != null) {
            bindingResult.getFieldErrors().forEach(violation -> {
                String message = violation.getDefaultMessage();
                String field = violation.getField();
                ValidationError error = new ValidationError();
        return errors;

I commented out the line that sets the code because, as I mentioned above, I'm not using that here.

Also note that the method returns an empty List, rather than a null, if there are no errors.


The ResponseEntityUtil class includes convenience methods that create ResponseEntity objects. 

If you're unfamiliar with the ResponseEntity class, it's part of the Spring framework. I use it as a convenient way to send back an HTTP status with a response at the same time.

Here's a method in ResponseEntityUtil that creates a basic ResponseEntity with the three (3) important elements of a response:

    public static <T> ResponseEntity<IRestResponse<T>> createResponseEntity(IRestResponse<T> response) {
        return ResponseEntity

Once again: note the use of the generic. That's important here because the T defines the type of response sent back.

Also keep in mind the ReponseEntity also uses a generic. The type parameter in the code above is IRestResponse.

That makes sense because, in this API, every response will include a response status, an HTTP status code, and the response itself. All three (3) of those pieces of info can be gleaned from an IRestResponse object.

The body of the method constructs the ResponseEntity object with the assistance of the status() static method. That method accepts the HTTP status as an int value.

Finally, the method sets the actual response with the body() method. That's the part that gets included in the response body.

Here are a couple of convenience methods in ResponseEntityUtil for creating error and success response statuses:

    private static ResponseStatus getErrorResponseStatus(String message) {
        ResponseStatus status = new ResponseStatus();
        return status;
    private static ResponseStatus getSuccessResponseStatus(String message) {
        ResponseStatus status = new ResponseStatus();
        return status;

Both of those methods accept a String message that gets sent back with the response status. You saw examples of those messages in the sample responses above.

Here's a convenience method that sends back a response with validation errors:

    public static ResponseEntity<IRestResponse<List<ValidationError>>> createResponseEntityWithValidationErrors(List<ValidationError> errors) {
        RestResponse<List<ValidationError>> fullResponse = new RestResponse<>();
        if (errors != null && errors.size() > 0) {
            ResponseStatus responseStatus = getErrorResponseStatus("Validation error");
        return createResponseEntity(fullResponse);

That method starts off by creating a response status with the message "Validation error."

After that, it populates the RestResponse object with the list of errors, the just-created response status, and an HTTP status code of 400 (Bad Request).

Then it invokes the createResponseEntity() method you already saw to construct the ResponseEntity object.

Here's an example of a convenience method that returns a successful response:

    public static <T> ResponseEntity<IRestResponse<T>> createSuccessfulResponseEntity(String message, int httpStatusCode, T response) {
        ResponseStatus status = getSuccessResponseStatus(message);
        RestResponse<T> fullResponse = new RestResponse<>();
        return createResponseEntity(fullResponse);

That method accepts the response status message and the HTTP status code in addition to the response itself.

Why? Because a successful HTTP response status code could be a few different options in the 200's. And the message itself might vary from one success to another.

Ground Controller to Major Tom

Thus far you've laid the groundwork, now let's put it to use.

Here's a controller method that creates a new user from a registration page in the application:

    public ResponseEntity<IRestResponse<User>> createUser(@Valid @RequestBody Registrant registrant, BindingResult bindingResult) {
        LOG.debug("Registrant is " + registrant);
        List<ValidationError> validationErrors = ValidationUtil.convertBindingResultToValidationErrors(bindingResult);
        //look for any validations not caught by JSR 380
        registrantService.validateRegistrant(registrant, validationErrors);
        User savedUser = registrantService.saveUser(registrant);
        return ResponseEntityUtil.createSuccessfulResponseEntity("Successfully registered!", HttpStatus.CREATED.value(), savedUser);

That's a standard method that exposes an endpoint in a Spring Boot REST controller.

First, note that the method is annotated with @PostMapping because, like most requests that persist a new entity, it's accepting an HTTP POST method.

The method returns a type of ResponseEntity. As I pointed out previously, that's a great tool to use with Spring controllers.

And, of course, the ResponseEntity type parameter is IRestResponse. The IRestResponse definition the type parameter User because that's what the client will get back in the event of a successful request.

The method, intuitively named createUser() accepts two parameters: a Registrant object and a BindingResult object.

The @Valid annotation in front of Registrant tells Spring to validate it. The @RequestBody annotation tells Spring that it needs to translate the JSON request body to that Java Registrant class you saw several sections ago.

The BindingResult class is where Spring stores the validation results. That's all handled by the framework.

Inside the method, the code invokes that static convertBindingResultToValidationErrors() method that translates the BindingResult object to a List of ValidationError objects. You saw that method earlier.

Next, the code invokes the validateRegistrant() method on the RegistrantService object. In the event of any validation errors, that method will throw a runtime exception that Spring will translate to an error response. I'll show you that in a moment.

In the meantime, you might be wondering why there's another layer of validation. It's for a good reason, and the comments above that validateRegistrant() line give the game away.

Those cute little annotations in the Registrant class will catch many validation errors. But they won't catch all of them.

For example, you want to make sure the user doesn't enter an email address that's already in use. That doesn't get handled with those annotations.

So that second layer of validation exists. Now let's look at the code:

    public void validateRegistrant(Registrant registrant, List<ValidationError> errors) {
        validateUniqueName(errors, registrant);
        validateUniqueEmail(errors, registrant);
        validateRecaptcha(errors, registrant);
        LOG.debug("validation is " + errors);
        if (errors.size() > 0) {
            throw new InvalidRegistrantRequestException(errors);

There you see three additional validation checks (unique name, unique email address, and captcha validation).

You don't need to know about the details of those methods. Just know that any failure in those validation checks results in a new ValidationError object going into the errors List.

After all the checks, the code looks at errors to see if it's got anything in it (the size is more than 0). If so, it throws an InvalidRegistrantRequestException. Let's look at that next.


InvalidRegistrantRequestException is a runtime exception. That's why it doesn't need to be caught in the code you see above.

Here's what it looks like:

public class InvalidRegistrantRequestException extends RuntimeException {

    private List<ValidationError> errors;
    public InvalidRegistrantRequestException(List<ValidationError> errors) {
        super("Registrant validation failed!");
        this.errors = errors;
    public List<ValidationError> getErrors() {
        return errors;

As you can see, the public constructor accepts a list of validation errors. Those are the errors that get used to create the ResponseEntity object that eventually gets returned to the client.

But how does it get returned? With the aid of an exception handler.

The Exception Handler

The Spring framework makes it easy to handle exceptions and transform them into exactly the types of responses you need to send back to clients. One of the ways it does that is with the assistance of exception handlers.

Back in that same controller class you were looking at earlier, add this method:

    public ResponseEntity<IRestResponse<List<ValidationError>>> invalidRegistrant(InvalidRegistrantRequestException ex) {
        List<ValidationError> errors = ex.getErrors();
        return ResponseEntityUtil.createResponseEntityWithValidationErrors(errors);

First of all, the way to identify a method that's an exception handler is with the appropriately named @ExceptionHandler annotation.

But you also have to include the type of exception it handles. You'll see that in the parentheses.

The method itself is structured like just about any other method in a controller. It accepts various input parameters and returns a ResponseEntity type.

Note that one of the parameters is the exception object itself. Yep, you can do that with an exception handler.

The body of the method uses that exception object to construct an error response using the createResponseEntityWithValidationErrors() method you saw earlier.

So to recap: if there are any validation errors at all, that method above gets called and the client receives an error response.

Back to the Other Part of the Controller

Let's revisit that other method:

    public ResponseEntity<IRestResponse<User>> createUser(@Valid @RequestBody Registrant registrant, BindingResult bindingResult) {
        LOG.debug("Registrant is " + registrant);
        List<ValidationError> validationErrors = ValidationUtil.convertBindingResultToValidationErrors(bindingResult);
        //look for any validations not caught by JSR 380
        registrantService.validateRegistrant(registrant, validationErrors);
        User savedUser = registrantService.saveUser(registrant);
        return ResponseEntityUtil.createSuccessfulResponseEntity("Successfully registered!", HttpStatus.CREATED.value(), savedUser);

If registrantService.validateRegistrant() doesn't throw an exception, the code above will persist the registrant as a User object.

And then it returns that persisted User object in a success response.

There's the code. Now it's time to test it out.

The Postman Always Rings Twice

Now fire up the Spring Boot application with all this wonderful new code. Then launch Postman to do some testing.

Start by creating a request that intentionally leaves out the last name. Like this:

    "firstName": "brian",
    "password": "allowishes",
    "emailAddress": "",
    "phone": "919-555-1212",
    "username": "george"

POST that to your endpoint and you should get something like this:

    "responseStatus": {
        "statusCode": "ERROR",
        "message": "Validation error"
    "response": [
            "field": "lastName",
            "defaultMessage": "must not be null"

In fact, that's exactly what I'm getting:


Pay attention to the HTTP status code where you see the red arrow. That's exactly what it should be (400 for Bad Request).

Now make sure the array works. Take out the first name in addition to the last name and run it again.

You'll get this:


Cool. So the API returns all validation errors at once. That gives the user the opportunity to fix them all at once.

Now try a successful response.


Awesome. It worked.

I should probably clear those nulls out of the response. But that's something I can take care of later.

Wrapping It Up

This was a fairly lengthy guide. In the event I missed something, you're more then welcome to look at the code I have on GitHub.

The utility classes/methods are in their own project and included as a dependency in the API. You can find them here.

If you want to see the API code (still a work in progress as of this writing), you can look at it here

Then it's up to you to write some code yourself. Take the patterns that you saw in this article and make them your own.

Have fun!

Photo by Poppy Thomas Hill from Pexels