Matthew M Dalby

REST: Exception Handling

A standardized approach to exception handling in REST APIs.

Posted on January 20, 2025

Introduction

Consistent exception handling is critical for effective API design. While there is truly no hard standard for reporting exceptions, I will share my experiences and opinions here.

This article will focus on exception handling from within a REST focus, as I have a tendency to work a lot in this area.

To illustrate how these concepts may be applied across different stacks, and are pretty much framework agnostic, I will provide examples in Java and Node.js stacks. Fully functional projects are available on GitHub under the following locations.

I have been working with REST APIs for over a decade now, across various projects and organizations. To me the concepts what I am presenting here seem self-evident, however I am often surprised at the amount of what could be described as 'anti-patterns'. I hope this articles either confirms your current processes are correct, or perhaps allows you to indentify opportunities for improvement in your existing code base.

There are a few important areas I will cover in this article as follows.

  • Apply an effective validation policy
  • Utilize custom exceptions
  • Provide consistency in response codes

Apply an effective validation policy

So whenever possible, I highly recommend using a well though out approach towards validation. If you can capture invalid data as early as possible, you you can respond with a concise response.

For example, when attempting to perform an update (HTTP PUT/PATCH) operation, a check to confirm that the targeted entity for the update operation actually exists, and if it does not exist, ideally we would return and 404 response code. The issue is the default result for attempting to update a non-existent entity will more than likely throw an exception that would probably result in an 500 response code.

In the java world, the Java Bean Validation API provides an intuitive way to annotate objects with validation constraints. So in the following example illusts an annotated entity and via the @Valid annotation, the runtime framework is instructed to automatically apply attribute level validation.

+ Show More
1// An object annotated with validation logic..
2public class Product{
3    private int id;
4
5    @NotNull
6	@Size(max=64)
7    private String name;
8}
9
10// An controller entry point implementing auto validation...
11@RESTController
12public class ProductController{
13
14    @PostMapping
15    public ResponseEntity<ProductVo> saveProduct(@Valid @RequestBody Product product){
16
17    ...
18
19    }
20}
+ Show More

This is a standard approach for that particular tech stack, however for NodeJS there is less of a standard, so many other options exist.

My main point here is validate at a detailed level, and as early as possible.

Utilize custom exceptions

I am a big advocate of utilizing custom exceptions. This allows you to create a hierarchy of exceptions that are specific to your domain and application. This is important as it allows you to handle exceptions in a more granular way, and provide more meaningful error messages to the client.

A custom handler in Node.js
+ Show More
1
2export abstract class BaseApplicationException extends Error{
3
4    /** Custom application level codes, intended for monitoring functions. */
5    appCode:string; 
6    /** HTTP status code that will ultimately be returned in the ressponse payload. */
7    httpStatusCode:number;
8    /** Whereas the message provides a more customizable reason for the error, this code is 
9     * standardized for all instances of this error. */
10    defaultMessage:string;
11
12    /**
13     * 
14     */
15    constructor( message: string, appCode:AppErrorCodes, httpStatusCode:number, 
16      defaultMessage:string) {
17    super(message);
18    this.appCode = appCode;
19    this.httpStatusCode = httpStatusCode;
20    this.defaultMessage = defaultMessage;
21  }
22}
23
24/**
25 * Exception intended for cases where a request to create or update an entity fails due to business 
26 * validation logic.
27 */
28export class EntityValidationException extends BaseApplicationException{
29    
30    constructor(message:string){
31        super(message,AppErrorCodes.ENTITY_VALIDATION_ERR, HTTP_STATUS_CODE_BAD_REQUEST, 
32            "Entity validation failure");
33    }
34}
35
36
+ Show More
And an example in Java
+ Show More
1package com.wc_matthew.demo.exception_handling.exception;
2
3import org.springframework.http.HttpStatusCode;
4
5/**
6 * BaseException
7 * 
8 * Parent class for all custom application exceptions. 
9 * 
10 */
11public abstract class BaseException extends RuntimeException{
12	
13	public BaseException() {
14        super();
15    }
16
17	public String customMessage;
18	public String errCode;
19	public HttpStatusCode httpStatusCode;
20	
21	/**
22	 * All instances of custom exceptions used additional attributes in 
23	 * conjunction with just a standard message. 
24	 * 
25	 * @param message Standard message for all instances of a given type.
26	 * @param customMessage, provides additional contextual information, such as the 
27	 * reason for why attribute or business logic fails.... 
28	 * @param errCode Custom error code used to uniquely identify the exception 
29	 * type, intended for use by external monitoring applications.
30	 */
31    public BaseException(String message, String customMessage, String errCode) {
32        super(message);
33        this.customMessage = customMessage;
34        this.errCode = errCode;
35    }
36	
37}
38
39public class EntityValidationFailureException extends BaseException{
40	
41	public EntityValidationFailureException(String detailMsg) {
42		super("Entity validation failure", detailMsg, ErrorConstants.ENTITY_VALIDATION_ERR);
43	}
44}
+ Show More

In both examples, we have create a base class, for which purpose specific implementations are created. Basically we wrap an additional message element, and an exception code that makes it easy to classify the exception from an auditing perspective. Given a centralized exception approach, which we will cover later in this article, this helps to maintain a consistent exception handling strategy.

Provide consistency in response codes

An important consideration if to provide the correct code for error type conditions. I have seen on mulitple occasions something like the following.

+ Show More
1fetch ('http://myapi.com')
2.then((rawData)=>rawData.json())
3.then((jsonData)=>{
4	
5	// Manually check the message to determine if the operation actually suceeded
6	if(jsonData.status=="error"){
7
8		...
9	}
10
11	// Other valiations
12	if(jsonData.success=="true"){
13	if(jsonData.status=="succeeded"){
14
15});
+ Show More

So the above example is a Javascript snippet that is prepared to handle exceptions that are masked by HTTP 200 response codes. The issue here is that the Fetch API is natively prepared to handle 500 response codes. Adding in cheks for a status message in the body to determine if the operation truly succeeded requires unecessary effort. Additionally, a 500 response code, using an alternate approach leaves more options. Above we see each developer may return their own preferred status indicators. That example is something based on real world past experience. One particular project used this approach, and the request handlers would use additional criteria in the if condition, which was a mess. Typically when you observe this type of syntax, it is a sign of larger architectual issues.

In short, use 400 and 500 codes strictly. Stay out of the business of overriding logic that simple &works&

Implementing creational codes

For requests that create or update data, use the appropriate more granular HTTP status code. 201 (Created) and 204 (No content) may be used to further clarify the operation was successful.

Proper use of 404 exceptions

The use of 404 exceptions is a bit tricky as this could indicate if a resquested API endpoint does not physically exist, or if indeed it did, but the requested record did not. I frequently make use of a header containing an additional message that allows us to differentiate between the two cases. An additional entry in the response payload is another viable option, however the important thing is you provide the ability for the client to understand the true nature of the issue, ,and possibly any auditing middleware that might be responsible for exposing broken links.

Bringing visibility into actual exceptions

Finally, information regaring the nature of the error condition may be of use. You may not want to necessarily return the entire stack trace or details of if a database or dependency API is not available, however at a minimum returning a application specific code that might indicate which system dependency was not available may help to streamine the exception troublshooing process.

For example, if a support resource recieves a request from a user indicating they are experiencing an ' SE-10017 &pos;, the support runbook could direct the request initially to a resource responsible for the database system that is off line, rather than initially fielding it to the developer on call.

Centralizing exception handling

Governence of a consistent process for handling exceptions in your API is most easily accomplished when exception handling logic is centralized. So, let's look at the following examples of throwing exceptions at the logic level.

+ Show More
1
2// A javascript example
3function doSomething {
4    ...
5    throw new ValidationException("For some reason.....");
6}
7
8// And a java implementation
9public void doSomething(){
10    ...
11
12    throw new ValidationException("For some reason......");
13
14}
+ Show More

Clean, consistent, easy to throw runtime exceptions. Note that developer is abstracted from the details of the exception handling strategy, and the resulting response output. Establishing an centralized location for catching and handling exceptions acorss the application can be accomplished in Java via Servlet Filters, and within a Node.js stack via express Middleware. Examples are as follows.

Java based centralized exception handling
+ Show More
1package com.wc_matthew.demo.erp.core.middleware;
2
3import org.springframework.beans.factory.annotation.Autowired;
4import org.springframework.http.HttpHeaders;
5import org.springframework.http.HttpStatus;
6import org.springframework.http.ResponseEntity;
7import org.springframework.web.bind.annotation.ControllerAdvice;
8import org.springframework.web.bind.annotation.ExceptionHandler;
9
10import com.wc_matthew.demo.erp.core.exception.BaseException;
11import com.wc_matthew.demo.erp.core.exception.InvalidRequestException;
12import com.wc_matthew.demo.erp.core.service.ExceptionHandlerService;
13
14/**
15 * GlobalExceptionHandler 
16 * 
17 * Responsible for centralizing exception handling logic for exceptions (
18 * both custom thrown and un-handled).
19 * 
20 * The goal is to standardize exception handling, where details are returned in an 
21 * consistent manner across the application via HTTP headers.
22 * 
23 */
24
25@ControllerAdvice
26public class GlobalExceptionHandler {
27	
28	public static final String HEADER_ERROR_CODE = "app-err-code";
29	public static final String HEADER_DEFAULT_MESSAGE = "default-message";
30	public static final String HEADER_MESSAGE = "message";
31	
32	@Autowired 
33	ExceptionHandlerService exceptionService;
34	
35	@ExceptionHandler(InvalidRequestException.class)
36    public ResponseEntity<String> handleCustomException(InvalidRequestException ex) {
37        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
38    }
39	
40	/**
41	 * TODO: investigate RFC 9457 compliance (https://www.rfc-editor.org/rfc/rfc9457.html)
42	 * 
43	 * @param ex
44	 * @return
45	 */
46    @ExceptionHandler(Exception.class)
47    public ResponseEntity<String> handleGlobalException(BaseException ex) {
48    	
49    	// Broadcast exception to the generic handler, which in turn may perform
50    	// operations such as reporting to a third party API for enhanced 
51    	// error handling.
52    	exceptionService.logException(ex);	
53    	
54    	HttpHeaders responseHeaders = new HttpHeaders();
55    	
56    		BaseException customEx = (BaseException)ex;
57    		
58            responseHeaders.set(HEADER_ERROR_CODE, customEx.getErrCode());
59            responseHeaders.set(HEADER_DEFAULT_MESSAGE, customEx.getMessage());
60            responseHeaders.set(HEADER_MESSAGE, customEx.getCustomMessage());
61        
62            ResponseEntity<String> response = new ResponseEntity<String>(customEx.getMessage(), customEx.getHttpStatusCode());
63            return response;		         
64       }
65    
66}
67
+ Show More
Node based centralized exception handling
+ Show More
1const ServerErrorException = require('../exception/server-error.exception');
2const ValidationException = require('../exception/validation-failure.exception');
3const EntityNotFoundException = require('../exception/entity-not-found.exception');
4const reportError = require('../utils/error-repoprting.utils');
5/**
6 * Middleware dedicated to centralizing logic related to exception handling
7 * related operations.
8 * 
9 * @param {*} error 
10 * @param {*} req 
11 * @param {*} res 
12 * @param {*} next 
13 * @returns 
14 */
15const errorHandlerMiddleware = (ex, req, res, next)=>{
16    console.log(`middleware: exception caught`);
17    console.warn(ex);
18    if(ex){
19        
20        // Call hook to broadcast out error event to any configured 
21        // third party exception handling services.
22     reportError(ex);
23
24        if(ex instanceof ValidationException){
25            console.log(`mw: validation error caught`);
26            res.status(400);
27            res.set('messsage','validation-failed');
28            res.send({
29                'validation-message': ex.message
30            });
31            next();
32        }
33        else if(ex instanceof ServerErrorException){
34            res.status(500);
35            res.set('messsage','Internal server exception');
36            res.send({});
37        }
38        else if(ex instanceof EntityNotFoundException){
39            res.status(500);
40            res.set('messsage','Entity not found exception');
41            res.send({'message':ex.message});
42        }
43        else{
44            res.status(500);
45            res.set('messsage','Unhandled exception');
46            res.send({});
47        }
48        
49        return;
50    }
51    console.log(`all good!`);
52    next();
53};
54
55module.exports = errorHandlerMiddleware;
+ Show More

So the above two example accomplish the same result, watch for certain exception types, catch them, and then return a consistent result. Since we making use of custom exceptions here, this streamlines the process of including custom codes in headers. From a developers perspective, the syntax is very similar to standard exceptions, so there is very little overhead associated with using them, which helps to increase adoption.

In Summary

So, consistency, consistency, consistency. Centralize logic, make use of custom exceptions, return consistent messaged, using established use cases.

Again, two functioning projects are available at the following locations on GitHub that illustrate these concepts in both Node.js and Java stacks.