Skip to main content
  1. « Go back to Articles

Implementing an HTTP error class in TypeScript

Image of the PropsForStyling type code

Being able to throw a dedicated HttpError error object in backend code turned out to be useful in nearly every project, as it allows you to clearly communicate the reason an exception occured and which status code your endpoint handler should set in the response. Node.js includes several error classes and many of error codes. However, these are mostly thrown on exceptions that occur on the language or system level. We need to implement the desired HttpError ourselves.

HTTP error codes and messages #

Let’s start by gathering a list of all HTTP error codes and names and put them into an object. I like to use https://httpstatuses.io due to their clear layout and concise information.

// From https://httpstatuses.io
const httpErrorMessages = {
  // 4×× Client Error
  400: 'Bad Request',
  401: 'Unauthorized',
  402: 'Payment Required',
  403: 'Forbidden',
  404: 'Not Found',
  405: 'Method Not Allowed',
  406: 'Not Acceptable',
  407: 'Proxy Authentication Required',
  408: 'Request Timeout',
  409: 'Conflict',
  410: 'Gone',
  411: 'Length Required',
  412: 'Precondition Failed',
  413: 'Payload Too Large',
  414: 'Request-URI Too Long',
  415: 'Unsupported Media Type',
  416: 'Requested Range Not Satisfiable',
  417: 'Expectation Failed',
  418: "I'm a teapot",
  421: 'Misdirected Request',
  422: 'Unprocessable Entity',
  423: 'Locked',
  424: 'Failed Dependency',
  426: 'Upgrade Required',
  428: 'Precondition Required',
  429: 'Too Many Requests',
  431: 'Request Header Fields Too Large',
  444: 'Connection Closed Without Response',
  451: 'Unavailable For Legal Reasons',
  499: 'Client Closed Request',

  // 5×× Server Error
  500: 'Internal Server Error',
  501: 'Not Implemented',
  502: 'Bad Gateway',
  503: 'Service Unavailable',
  504: 'Gateway Timeout',
  505: 'HTTP Version Not Supported',
  506: 'Variant Also Negotiates',
  507: 'Insufficient Storage',
  508: 'Loop Detected',
  510: 'Not Extended',
  511: 'Network Authentication Required',
  599: 'Network Connect Timeout Error',
} as const

We declare this object as const, because the data doesn’t change at runtime and we’ll derive several types from this dataset as follows:

type HttpErrorMessages = typeof httpErrorMessages
//   ^
//   type HttpErrorMessages = {
//     readonly 400: "Bad Request";
//     readonly 401: "Unauthorized";
//     ...
//   }

type HttpErrorCode = keyof HttpErrorMessages
//   ^
//   type HttpErrorCode = 400 | 401 | 402 | ...

type HttpErrorMessage<TCode extends HttpErrorCode> = HttpErrorMessages[TCode]
// type BadRequest = HttpErrorMessage<400>
//      ^
//      type BadRequest = "Bad Request"

We implemented the HttpErrorCode type that is a literal union type of all numeric status codes, and the lookup type HttpErrorMessage that accepts a status code as type parameter and returns the error message as string literal type.

The HttpError class #

Using these types, we implement the HttpError class by extending the CustomError class from the ts-custom-error package. Unfortunately, simply extending the native Error class doesn’t yield the desired result, as is explained in the Why?-section of their documentation.

import { CustomError } from 'ts-custom-error'

class HttpError<
  TCode extends HttpErrorCode,
  TMessage extends HttpErrorMessage<TCode>,
> extends CustomError {
  code: TCode
  private _message: TMessage
  private _customMessage?: string

  constructor({ code, message }: { code: TCode; message?: string }) {
    super()
    this._message = httpErrorMessages[code] as TMessage
    this._customMessage = message
    this.code = code
  }

  get message() {
    return this._customMessage ?? this._message
  }
}

This is a simple and pragmatic implementation of an HttpError class. You could, of course, separate original and custom error messages in public class attributes, define additional fields in the constructor, utilize a builder pattern, implement an error factory, etc etc.

We included the TMessage type parameter in the class declaration, which is not strictly neccessary. As an advantage, however, we gain the ability to see the default error message in the tooltip when hovering the error object with the mouse:

const E1 = new HttpError({ code: 400 })
//    ^
//    HttpError<400, "Bad Request">

const E2 = new HttpError({ code: 401, message: 'User not found' })
//    ^
//    HttpError<401, "Unauthorized">

I found this very useful and convinient when reading code that throws different HttpErrors in several places. I usually can’t remember more than a handful of the most common error types and thus, don’t have to look up the more uncommon types somewhere else.

Identify HttpError in exception handlers #

Thanks to the ts-custom-error package, we can identify instances of the HttpError class in an exception handler by using the instanceof operator:

try {
  // Do something that might fail and throw an error:
  throw new HttpError({ code: 400 })
} catch (error) {
  if (error instanceof HttpError) {
    console.error(`❌ [${error.code}] ${error.message}`)

    // Send the corresponding response, e.g. in Express.js:
    res.status(error.code).send(error.message)
  } else {
    // Handle other errors differently
  }
}
Dr. Ole Hüter
Author
Dr. Ole Hüter
Freelance Full Stack Web Developer