Handling errors in REST APIs

Shit always happens! so be ready to deal with it.

By Wutsi Tech Blog
5 Minutes

The only constant in software engineering: shit always happens! So when designing your API, you must think about how to deal with errors to help your clients the to better use it.

In this article, we've explained the concept of layered architecture for REST API. In this article we are going to show to manage errors and return them to clients, in the context of REST API designed with layered architecture.

Categories of Errors

There are 4 categories of errors that you'll have to deal with when building an API:

  • Validation errors
  • Security errors
  • Business errors
  • Technical errors
Errors in Layered Architecture
Errors in Layered Architecture

Validation Errors

Validation errors occur when clients send invalid requests. For example:

  • Searching a user by email using the value ray.sponsible, which is an invalid email value.
  • Publishing a Story without title, which is a required information. 

The Presentation Layer has the responsibility of detecting validation error, and protect the other layers from processing invalid requests.

Security Errors

Security errors occur when clients perform unauthorized operations. The 2 types of security errors:

  • Unauthenticated errors: An anonymous user try to perform an operation that requires authentication. Example: A user try to create a new Story, but he is not logged in!
  • Forbidden errors: A user try to perform and operation he is not allowed to. Example: A user try to modify a Story published by another user.

The Presentation Layer has the responsibility of detecting security errors, and protect other layers of from any security compromission.

Business Errors

Every system has a set of rules that define or constraint business activities. For example:

  • User emails must be unique
  • User cannot follow their own blog

A business error occurs when any of the business rule is broken. Business errors are detected at the Business layer, since this is where business related processing is performed.

Technical Errors

Technical errors all the other errors, usually related to the environment when the system is running:

  • Errors from Persistence or Database layers
  • Errors from the environment: hardware, file system, network etc
  • Bugs (Ex: NullPointerException)

REST API Errors

HTTP protocol uses status codes to indicate if any request has been successfully completed.  REST API leverage HTTP status code for returning errors to their clients.

Validation Errors

  • Return the status code 400 to indicate that their request is not valid.

Security Errors

  • Return the status code 401 to indicate that anonymous user performs an operation that requires authentication.
  • Return the status code 403 to indicate when a user try to perform an operation that he is not suppose to.

Business Errors

  • Return the status code 409 to indicate a business rule has been broken
  • Return the status code 404 to indicate that the information requested were not found

Technical Errors

  • Return the status code 500 to indicate technical error

An Example: Registering a user in a Blog

Now, let see some code that show how to deal with errors, using the use case for registering a user in a Blog. To register a new user via the Blog API, the client must provide the following information:

  • email: it's required and must be unique
  • password: it's required and must have at least 8 characters
  • display name: User first and last name. It's optional

Create the Exceptions

The first step for handling errors is to create Exception classes for each HTTP status code that will be returned by the API.

For our example about creating a new user in our Blog platform:

  1. if the email is email, password provided are empty or has less than 8 characters, we will consider it as validation error.
  2. If the user email is not unique, we will consider it as a business error. Non unique emails is in violation of the business constraint enforcing unicity of emails.
/* For Validation Errors */
class ValidationException(code: String, description: String): Exception() {
}

/* For Business Errors */
class BusinessException(code: String, description: String): Exception() {
}

Errors in Business Layer

This is the code in UserService for creating new user:

class UserService (
   dao: UserRepository
) {
   fun create(request: CreateUserRequest) {
      ensureEmailUnique(request.email)
      
      user = createUser(request)
      dao.save(user)
   }

   /** 
    * Verify the business rule: email must be unique.
    * If the rule is broken, we throw BusinessException
    */
   private fun ensureEmailUnique(email: String) {
      if (dao.findByEmail(email) != null) 
         throw BusinessException("duplicate_email", "There is already an account associated with $email")
   }

   private fun createUser(request: CreateUserRequest): User {
     ...
   }
}

Errors in Presentation Layer

This is the code in UserController for handling the REST request for registering new user:

class UserController(
  service: UserService
) {
  fun create(request: CreateUserRequest): CreateUserResponse {
     validate(request)

     user = service.create(request)
     return toCreateUserResponse(user)
  }

  private fun validate(request: CreateUserRequest) {
     if (request.email.isNullOrEmpty())
       throw ValidationException("email_missing", "email is required")

     if (request.password.isNullOrEmpty())
       throw ValidationException("password_missing", "password is required")
     if (request.password.length < 8)
       throw ValidationException("password_too_short", "password should have at least 8 characters")     
  }

  private toCreateUserResponse(user: User): CreateUserResponse {
   ...
  }
}

Error Handling

After raising the errors in the different layers, the last step is to handle them.  Error handling should be done only at Presentation Layer, because this is where errors will be converted to response to the clients (status code + error details)

This is how the code looks like in the UserController,  integrated with exception handling:

class UserController(
  service: UserService,
  exceptionHandler: ExceptionHandler
) {
  fun create(request: CreateUserRequest): CreateUserResponse {
     try {

       ensureValid(request)

       user = service.create(request)
       return toCreateUserResponse(user)

     } catch(ex: Exception) {
       exceptionHandler.handle(ex)
     }
  }
}
class ExceptionHandler {
  fun handle(ex: Exception) {
     if (ex is NotFoundException) {
         sendError(404, ex.code, ex.message)
     } if (ex is BusinessException) {
         sendError(409, ex.code, ex.message)
     } else if (ex is ValidationException) {
         sendError(400, ex.code, ex.message)
     } else {
         sendError(500, "unexpected_error", ex.message)
     }
  }

  fun sendError(statusCode: Int, code: String, message: String) {
    // Return to user the HTTP status code..
    // And the error details in JSON format { 'code': ..., 'message': ... }
    // ....
  }  
}

In Summary

  • Create Exception class for each status codes you expect to return to your clients.
  • Fire business errors in the Business Layer.
  • Fire validation and security errors in the Presentation Layer.
  • Handle all errors in the Presentation layer, by returning to client the appropriate status code and error details to the client.
REST Api
Exception Handling