JS Error Handling

This is my vademecum on error handling in JavaScript

How to define an error

Create an error extending the Error class.

export class ErrorInvalidArg extends Error {
	static errorName = "ErrorInvalidArg"
	static message = "Invalid argument"
	constructor() {
		super(ErrorInvalidArg.message)
	}
}

Note that a static attribute errorName with the name of the error, i.e. the class name, is added to be able to serialize the error. Using ErrorInvalidArg.name may not work if the code is minified, hence an explicit string must be added.

After calling super in the constructor, set the name as the static errorName otherwise the error instance will have default name “Error”.

It is not worth to extend other error classes, (e.g. TypeError, RangeError) as they are used to categorize errors thrown by JavaScript and extending them would pollute what consumers expect to be on that list.

It is handy for the consumer that the class name is prefixed with Error. However break rules when it makes sense, for instance:

export class InternalServerError extends Error {
	constructor() {
		super("500")
	}
}

The constructor need to call super first, passing it the error message.

Testing errors

Error message is defined as a static method, it can be used to recognize the error in some contexts, for example when testing.

import { strict as assert } from "node:assert";
import { describe, test } from "node:test";

class ErrorInvalidDate extends Error {
  static errorName = "ErrorInvalidDate";
  static message () { return "Invalid Date" };
  constructor() {
    super(ErrorInvalidDate.message());
  }
}

const truncateDate = (arg: Date) {
  if (arg.toString() === "Invalid Date") throw new ErrorInvalidDate();
  return arg.toJSON().substring(0, 10);
}

describe("truncateDate", () => {
  test("throws ErrorInvalidDate", () => {
    assert.throws(
      () => {
        truncateDate(new Date("0000-00-00"));
      },
      {
        name: ErrorInvalidDate.errorName,
        message: ErrorInvalidDate.message()
      }
    );
  });
});

Error info

Optionally add info attributes to the class, for example

/**
 * Generic HTTP Error.
 *
 * @example
 *
 * const response = await fetch(url)
 * if (!response.ok) throw new ErrorHTTP(response)
 *
 */
export class ErrorHTTP extends Error {
	static errorName = "ErrorHTTP"
	status: Response["status"]
	statusText: Response["statusText"]
	url: Response["url"]
	constructor(response: Response) {
		super(ErrorHTTP.message(response))
		this.status = response.status
		this.statusText = response.statusText
		const url = new URL(response.url)
		this.url = `${url.origin}${url.pathname}`
	}
	static message({
		status,
		statusText,
		url
	}: Pick<Response, "status" | "statusText" | "url">) {
		return `Server responded with status=${status} statusText=${statusText} on URL=${url}`
	}
	toJSON() {
		return {
			name: ErrorHTTP.errorName,
			info: {
				status: this.status,
				statusText: this.statusText,
				url: this.url
			}
		}
	}
}

Notice some info could be not defined or unknown.

export class ErrorItemNotFound extends Error {
	static errorName = "ErrorItemNotFound"
	static message(type: ErrorItemNotFound["type"]) {
		return `${type} not found`
	}
	id?: unknown
	type: "User" | "Project" | "Transaction"
	constructor({ id, type }) {
		super(ErrorItemNotFound.message(type))
		this.id = id
		this.type = type
	}
}

How to catch errors

Using if instead of switch looks better when using TypeScript. Notice also that the correct type for the catched error is unknown.

try {
	// code
	throw new MyError()
} catch (error) {
	if (error instanceof MyError) {
		// handle it
	}
	// otherwise
	throw error
}

However, using error instanceof MyError can be done only if the error instance was created in the same JavaScript context that catches it. This could be not the case, not only in client-server model but also when using threads (e.g. Web Workers).

Serializable errors

An error should also be serializable into JSON, in the following example the toJSON() method return something that can be serialized; it will be internally called by JSON.stringify.

export class MyError extends Error {
	readonly bar: boolean
	readonly quz: number
	readonly whenCreated: number

	static errorName = "MyError"
	static message() {
		return "Something went wrong"
	}

	static isMyErrorData(arg: unknown): arg is MyErrorData {
		if (!arg || typeof arg !== "object") return false
		const { bar, whenCreated } = arg as Partial<MyErrorData>
		return (
			typeof bar === "boolean" &&
			typeof whenCreated === "number" &&
			whenCreated > 0
		)
	}

	constructor({ bar, quz }: Pick<MyError, "bar" | "quz">) {
		super(MyError.message)
		this.bar = bar
		this.quz = quz
		this.whenCreated = new Date().getTime()
	}

	toJSON() {
		return {
			name: MyError.errorName,
			data: {
				bar: this.bar,
				quz: this.quz,
				whenCreated: this.whenCreated
			}
		}
	}
}

export type MyErrorData = Pick<MyError, "bar" | "whenCreated">