Custom Error Messages in Spring REST API

Create global or application-level Exception handlers and return Custom Error Messages in Spring REST APIs.

Overview

Effective communication is the key to healthy and efficient relationships. Interestingly, the same applies to any Client and Server relationships. The client’s request may succeed or fail on the server. However, the server should provide the most appropriate status code in either of the outcomes.

Although sending a correct status code is enough for a client to take real action based on the outcome of a request, in case of failures, the client may need more details about what went wrong. For example, failure details like the exception type and an error message can help clients log the error or provide appropriate failure messages to their clients.

This article will teach How to handle different failures and return Custom Error Messages from a Spring REST API. If you don’t know how to handle exceptions in Spring REST API, please read Spring Rest Service Exception Handling.

Return a Generic Error Message using @ResponseStatus

The most basic way of returning an error message from a REST API is to use the @ResponseStatus annotation. We can add the error message in the annotation’s reason field. Although we can only return a generic error message, we can specify exception-specific error messages.

Next is an example of a @ControllerAdvice using @ResponseStatus annotations to return exception-specific error messages.

@ControllerAdvice
public class ApplicationExceptionHandler {
  
  @ResponseStatus(
      value = HttpStatus.NOT_FOUND,
      reason = "Requested Student Not Found")
  @ExceptionHandler(StudentNotFoundException.class)
  public void handleException(StudentNotFoundException e) {
  }

  @ResponseStatus(
      value = HttpStatus.BAD_REQUEST,
      reason = "Received Invalid Input Parameters")
  @ExceptionHandler(InputValidationException.class)
  public void handleException(InputValidationException e) {
  }

  @ResponseStatus(
      value = HttpStatus.GATEWAY_TIMEOUT,
      reason = "Upstream Service Not Responding, Try Again")
  @ExceptionHandler(ServiceUnavailableException.class)
  public void handleException(ServiceUnavailableException e) {
  }
}Code language: Java (java)

The Exception handler class has three exception handlers, each of which returns a specific HTTP Response Status. Each response status specifies a reason field with a particular error message.

To view the error message in the response, ensure you have turned on include-messages in the server configuration. To learn more about Spring Boot server configurations, please visit Spring Boot Embedded Tomcat Configuration.

server:
  error:
    include-message: alwaysCode language: YAML (yaml)

Next is an example of a response object the REST API returns. Note that the response object has the specified error message.

{
  "timestamp": "",
  "status": 404,
  "error": "Not Found",
  "message": "Requested Student Not Found",
  "path": "/students/Jack"
}Code language: JSON / JSON with Comments (json)

Although we can specify exception-specific error messages, it is still not informative. Therefore in the following sections, we will learn how to return a more specific error message from Spring REST API.

Return Error Message Using Custom Error Object

Let’s create a class representing the error message and the status code. We will return an instance of that in case of errors.

Next is the Error class representing the status code and a String message. We use a few Lombok annotations that introduce regular getter and setter methods and a constructor using the final fields.

Custom Response Error Class

@Data
@RequiredArgsConstructor
public class Error {
  private final HttpStatus httpStatus;
  private final String message;
}Code language: Java (java)

Now that we have an error model created, we will use it to return a detailed error message from Controller Advice.

@ExceptionHandler(StudentNotFoundException.class)
public ResponseEntity handleException(
    StudentNotFoundException e) {

  Error error = new Error(HttpStatus.NOT_FOUND, 
      e.getLocalizedMessage());
  return new ResponseEntity<>(error, error.getHttpStatus());
}Code language: Java (java)

The exception handler returns an instance of the Error class populated with the exception message and HTTP Status Code.

Now, we can throw our Not Found Exception with a custom error message.

throw new StudentNotFoundException
    ("Student service failed, studentId : " + studentId);Code language: Java (java)

When the REST API cannot find the requested resource, we get a detailed error as a response.

{
  "httpStatus": "NOT_FOUND",
  "message": "Student service failed, studentId : Jack"
}Code language: JSON / JSON with Comments (json)

Return Error Message Using HashMap

Also, instead of creating a dedicated error class, we can return a detailed error message using a simple HashMap. Next is an example of producing and returning a Custom Error Message using Java HashMap.

@ExceptionHandler(StudentNotFoundException.class)
public ResponseEntity<Map<String, String>> 
    handleException(StudentNotFoundException e) {

  Map<String, String> errorResponse = Map.of(
    "message", e.getLocalizedMessage(),
    "status", HttpStatus.NOT_FOUND.toString()
  );

  return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}Code language: Java (java)

Handle Bad Request Exceptions

The Bad Request errors are the Client errors where the client’s request doesn’t meet the requirements of the target server. This section will see how to handle Bad Request exceptions and provide a custom or detailed error response.

Type Mismatch Exceptions

The Type Mismatch Exceptions occur when Spring Controller cannot map the request parameters, path variables, or header values into controller method arguments. This section covers the handling of MethodArgumentTypeMismatchException and TypeMismatchException.

Spring throws MethodArgumentTypeMismatchException when the controller argument doesn’t have a required type. On the other hand, Spring throws TypeMismatchException when there is a type mismatch while setting Bean properties. Also, both exceptions provide a detailed error message that we can use to prepare the Error object.

To demonstrate that, next is an example of Handling MethodArgumentTypeMismatchException and TypeMismatchException and returning a detailed error message in Controller Advice.

@ExceptionHandler({
    MethodArgumentTypeMismatchException.class,
    TypeMismatchException.class
})
public ResponseEntity<Map<String, String>> 
    handleException(TypeMismatchException e) {
    
  Map<String, String> errorResponse = Map.of(
      "message", e.getLocalizedMessage(),
      "status", HttpStatus.BAD_REQUEST.toString()
  );

  return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}Code language: Java (java)

Note that the controller advice catches both exceptions; however, the method arguments accept an exception of type TypeMismatchException because it is the parent of the other exception.

Next, the snippet shows a detailed error message when we call a rest endpoint with an incompatible path variable leading to MethodArgumentTypeMismatchException.

{
  "httpStatus": "BAD_REQUEST",
  "message": 
      "Failed to convert value of type 'java.lang.String' 
       to required type 'java.lang.Long'; nested exception 
       is java.lang.NumberFormatException: For input 
       String: \"Jack\""
}Code language: JSON / JSON with Comments (json)

Bean Validation Exceptions

The Bean Validation exceptions occur when the request contents do not pass the provided validations.

The BindException occurs when the binding errors are fatal. While the MethodArgumentNotValidException occurs when validations specified by @Valid fail. Note that the MethodArgumentNotValidException is a subclass of BindException. Thus, we can handle them using the same Spring REST API’s exception handler.

@ExceptionHandler({
    BindException.class,
    MethodArgumentNotValidException.class
})
public ResponseEntity<Map<String, Object>> 
    handleException(BindException e) {
        
  List<String> errors = new ArrayList<>();
  e.getFieldErrors()
      .forEach(err -> errors.add(err.getField() + ": " + err.getDefaultMessage()));
  e.getGlobalErrors()
      .forEach(err -> errors.add(err.getObjectName() + ": " + err.getDefaultMessage()));

  Map<String, Object> errorResponse = Map.of(
      "error", errors,
      "message", e.getLocalizedMessage(),
      "status", HttpStatus.BAD_REQUEST.toString()
  );
  
  return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}Code language: Java (java)

Here we have created a List<String> to represent individual binding errors and add that to the response Map. Instead, we can add a List<String> field to the Error class we created in the previous section and populate the list with individual errors.

Handle Media Type Not Supported Exception

Spring throws HttpMediaTypeNotSupportedException, when a POST, PUT, or PATCH endpoint on the server cannot handle the content type sent by the client. The REST Controllers on the server specify the content type they can support. When the media type that a client sends doesn’t match, the client gets this exception back.

To demonstrate, next is an example of handling HttpMediaTypeNotSupportedException and returning a custom error response.

@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseEntity<Map<String, String>> handleException(
    HttpMediaTypeNotSupportedException e) {

  String provided = e.getContentType().toString();
  List<String> supported = e.getSupportedMediaTypes().stream()
      .map(MimeType::toString)
      .collect(Collectors.toList());

  String error = provided 
      + " is not one of the supported media types (" 
      + String.join(", ", supported) 
      + ")";

  Map<String, String> errorResponse = Map.of(
    "error", error,
    "message", e.getLocalizedMessage(),
    "status", HttpStatus.UNSUPPORTED_MEDIA_TYPE.toString()
  );
  return new ResponseEntity<>(errorResponse, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
}Code language: Java (java)

As seen in the exception handler above, the instance of HttpMediaTypeNotSupportedException provides detailed information about the incorrect media type we provided and a list of actually supported media types. Thus, we create a custom error message based on the available information.

{
  "error":"text/plain;charset=UTF-8 is not one of the supported media types (
      application/octet-stream, 
      text/plain, application/xml, 
      text/xml, application/x-www-form-urlencoded, 
      application/*+xml, 
      multipart/form-data, 
      multipart/mixed, 
      application/json, 
      application/*+json, */*)",
 "message":"Content type 'text/plain;charset=UTF-8' not supported",
 "status":"415 UNSUPPORTED_MEDIA_TYPE"
}Code language: JSON / JSON with Comments (json)

The above snippet shows a client’s sample error response when it sends a request with an invalid media type.

Handle Request Body Not Readable Exception

Now we will see an example of handling HttpMessageNotReadableException and returning a custom error response. The HttpMessageNotReadableException occurs when the request body is missing or unreadable.

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, String>> handleException(
    HttpMessageNotReadableException e) throws IOException {

  Map<String, String> errorResponse = Map.of(
      "message", e.getLocalizedMessage(),
      "status", HttpStatus.BAD_REQUEST.toString()
  );

  return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}Code language: Java (java)

Handle HTTP Request Method Not Supported Exception

The HttpMethodNotSupportedException occurs when the HTTP endpoint on the REST API does not support the HTTP request method. Let’s write an exception handler for HttpMethodNotSupportedException and return a detailed error message.

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<Map<String, String>> handleException(
    HttpRequestMethodNotSupportedException e) 
    throws IOException {
    
  String provided = e.getMethod();
  List<String> supported = List.of(e.getSupportedMethods());

  String error = provided 
      + " is not one of the supported Http Methods (" 
      + String.join(", ", supported) 
      + ")";

  Map<String, String> errorResponse = Map.of(
      "error", error,
      "message", e.getLocalizedMessage(),
      "status", HttpStatus.METHOD_NOT_ALLOWED.toString()
  );
  return new ResponseEntity<>(errorResponse, HttpStatus.METHOD_NOT_ALLOWED);
}Code language: Java (java)

As seen in the exception handler above, the exception instance provides detailed information about the provided HTTP Method and an array of Supported HTTP Methods. We use it to form a clear error message.

{
  "error": "GET is not one of the supported Http Methods (POST)",
  "message": "Request method 'GET' not supported",
  "status": "405 METHOD_NOT_ALLOWED"
}Code language: JSON / JSON with Comments (json)

The snippet showed an example response when the client attempted to execute a GET endpoint, while the REST API supports only POST.

Default Exception Handler

Similarly, we can create a default exception handler advice that handles all Exception types. Spring attempts to find the most specific handler when we have multiple exception handlers and falls back to the default handler if there is no suitable handler.

@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleException(
        Exception e) throws IOException {

  Map<String, String> errorResponse = Map.of(
      "message", e.getLocalizedMessage(),
      "status", HttpStatus.INTERNAL_SERVER_ERROR.toString()
  );

  return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}Code language: Java (java)

Above is an example of writing a default exception handler that returns an error message by the exception instance and an HTTP Status of 500.

Summary

This detailed tutorial taught us how to Return Custom Error Messages in Spring REST API. Firstly, we understood that Spring returns a generic error message and the most suitable HTTP Status Code by default. However, we can write our exception handlers for specific exceptions using @ControllerAdvice and produce a custom and detailed error response.

For more on Spring and Spring Boot Tutorials, please visit Spring Tutorials.