Implementing an HTTP error class in TypeScript
Table of Contents
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 HttpError
s 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
}
}