How to Create Custom Error Types in TypeScript

A TypeScript custom error tells your catch block what actually broke: bad input, a missing record or a declined card. A plain Error only says that something failed.

Quick answer: create a class that extends Error, call super(message) and set this.name. Then check it with instanceof in the catch block:

class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ValidationError";
  }
}

In this tutorial, we’ll build a TypeScript custom error step by step: extra properties, the cause option, a small error hierarchy with codes and safe handling of unknown in catch.

I ran every example under TypeScript 7.0.2 and Node.js v22.22.2 in strict mode, and they all use one sample: a small US checkout flow with ZIP codes, orders and card payments.

What You Need Before You Start With Custom Errors

  • TypeScript 4.6 or later and Node.js 16.9 or later for the cause option (our runs use the versions above).
  • "target": "es2022" or later in tsconfig.json, so classes compile to real class syntax.
  • "strict": true, which makes catch variables unknown instead of any.

You should be comfortable with a basic try/catch block. If not, read our guide to try/catch in TypeScript first, then come back.

Step 1: Extend the Error Class for a Custom Error Type

We start with input validation. The sample checks that a ZIP code has 5 digits and throws a ValidationError when it doesn’t:

class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ValidationError";
  }
}

function checkZip(zip: string): string {
  if (!/^\d{5}$/.test(zip)) {
    throw new ValidationError(`ZIP code "${zip}" must be 5 digits`);
  }
  return zip;
}

try {
  checkZip("7870");
} catch (err) {
  if (err instanceof ValidationError) {
    console.log(`${err.name}: ${err.message}`);
    console.log("instanceof Error:", err instanceof Error);
    console.log("stack starts:", err.stack?.split("\n")[0]);
  }
}

Output:

ValidationError: ZIP code "7870" must be 5 digits
instanceof Error: true
stack starts: ValidationError: ZIP code "7870" must be 5 digits
VS Code editor with a TypeScript ValidationError class that extends Error, terminal showing the error name, message, instanceof Error true and the first stack line
The catch block recognizes the error by class, prints its own name and still treats it as a normal Error.

The ZIP code 7870 has four digits, so the error prints ValidationError: ZIP code "7870" must be 5 digits. Setting this.name matters because the stack trace and logs use it.

instanceof works here because the class compiles to native class syntax. For other ways to check a value’s type at runtime, see checking types in TypeScript.

Step 2: Add Custom Properties and a Cause to a TypeScript Error

Same checkout app, now calling an API. An HttpError carries the status code and URL, plus a getter that decides whether a retry makes sense:

class HttpError extends Error {
  readonly status: number;
  readonly url: string;

  constructor(status: number, url: string, options?: ErrorOptions) {
    super(`Request to ${url} failed with ${status}`, options);
    this.name = "HttpError";
    this.status = status;
    this.url = url;
  }

  get isRetryable(): boolean {
    return this.status >= 500;
  }
}

function loadOrder(id: number): unknown {
  try {
    return JSON.parse("{ total: 149.99 }"); // bad JSON from the API
  } catch (cause) {
    throw new HttpError(502, `/api/orders/${id}`, { cause });
  }
}

try {
  loadOrder(4821);
} catch (err) {
  if (err instanceof HttpError) {
    console.log(err.message);
    console.log("status:", err.status, "| retry:", err.isRetryable);
    console.log("cause:", (err.cause as Error).message);
  }
}

Output:

Request to /api/orders/4821 failed with 502
status: 502 | retry: true
cause: Expected property name or '}' in JSON at position 2 (line 1 column 3)
VS Code editor with a TypeScript HttpError class with status and url properties, terminal showing the 502 message, retry true and the JSON parse cause
The custom error keeps the status and URL, and cause still holds the original JSON parse error.

The run prints status: 502 | retry: true, and the last line shows the original SyntaxError message. Passing { cause } as the second super() argument keeps the low-level error without losing your friendly one.

Note: ErrorOptions comes from the ES2022 lib, so set lib or target to es2022 or later. Mark properties readonly so handlers can’t change them; our TypeScript classes guide covers that keyword in depth.

Step 3: Build an Error Hierarchy With Error Codes

Bigger apps need more than one error class. We use an abstract AppError base, two subclasses and a string union of codes, then run three sample orders:

type ErrorCode = "NOT_FOUND" | "PAYMENT_DECLINED";

abstract class AppError extends Error {
  abstract readonly code: ErrorCode;
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = new.target.name; // NotFoundError, PaymentError...
  }
}

class NotFoundError extends AppError {
  readonly code = "NOT_FOUND";
}

class PaymentError extends AppError {
  readonly code = "PAYMENT_DECLINED";
  constructor(readonly amount: number) {
    super(`Card declined for $${amount.toFixed(2)}`);
  }
}

function checkout(sku: string, total: number): string {
  if (sku === "TX-404") throw new NotFoundError(`SKU ${sku} not found`);
  if (total > 500) throw new PaymentError(total);
  return `${sku} charged $${total.toFixed(2)}`;
}

const orders: [string, number][] = [
  ["TX-101", 89.5], ["TX-404", 20], ["TX-202", 649.99],
];

for (const [sku, total] of orders) {
  try {
    console.log("OK  ", checkout(sku, total));
  } catch (err) {
    if (!(err instanceof AppError)) throw err;
    switch (err.code) {
      case "NOT_FOUND":
        console.log("404 ", err.name, "-", err.message);
        break;
      case "PAYMENT_DECLINED":
        console.log("402 ", err.name, "-", err.message);
        break;
    }
  }
}

Output:

OK   TX-101 charged $89.50
404  NotFoundError - SKU TX-404 not found
402  PaymentError - Card declined for $649.99
VS Code editor with a TypeScript AppError abstract class, NotFoundError and PaymentError subclasses, terminal showing one charged order, a 404 NotFoundError and a 402 PaymentError
One order goes through, and the other two are routed by error code with the right subclass name.

TX-101 is charged $89.50, TX-404 becomes a 404 and the $649.99 order is declined. Each subclass gets its name from new.target.name, so you never repeat it.

Because code is a union of string literals, the switch narrows cleanly and typos fail at compile time. We compare that approach with enums in enums vs string literal unions.

Pro Tip: minifiers can rename classes, which changes new.target.name. If your logs depend on the name, set it as a string in each subclass.

Step 4: Narrow unknown Errors in the Catch Block

With strict on, a catch variable is typed unknown, because JavaScript lets you throw anything. Reading .message on it directly fails:

function parseTotal(raw: string): number {
  return JSON.parse(raw).total;
}

try {
  parseTotal("{ total: 149.99 }");
} catch (err) {
  console.log("Failed:", err.message);
}

Compiler output:

error_unknown.ts(8,26): error TS18046: 'err' is of type 'unknown'.
VS Code showing TypeScript error TS18046 'err' is of type 'unknown' on err.message inside a catch block
VS Code underlines err and the build stops with TS18046 before any JavaScript is written.

The fix is a type guard. We wrap the checks in a toMessage() helper and feed it a custom error, a JSON error and a thrown string:

class AppError extends Error {
  constructor(message: string, readonly code: string) {
    super(message);
    this.name = "AppError";
  }
}

function isAppError(e: unknown): e is AppError {
  return e instanceof AppError;
}

function toMessage(e: unknown): string {
  if (isAppError(e)) return `[${e.code}] ${e.message}`;
  if (e instanceof Error) return e.message;
  return String(e);
}

const jobs: (() => unknown)[] = [
  () => { throw new AppError("Coupon SPRING26 expired", "COUPON"); },
  () => JSON.parse("{ total: 149.99 }"),
  () => { throw "Payment service offline"; },
];

for (const job of jobs) {
  try {
    job();
  } catch (err) {
    console.log("Failed:", toMessage(err));
  }
}

Output:

Failed: [COUPON] Coupon SPRING26 expired
Failed: Expected property name or '}' in JSON at position 2 (line 1 column 3)
Failed: Payment service offline

All three print a clean message, and only the custom error shows its [COUPON] code. The e is AppError return type is explained in the is keyword in TypeScript, and any vs unknown shows why unknown is the safer default.

Do You Still Need Object.setPrototypeOf for Custom Errors?

Many older posts, including the first version of this one, add Object.setPrototypeOf(this, new.target.prototype) to every error class. That fix was only for ES5 output, where instanceof broke on subclasses of Error.

When we tried to compile for ES5 with TypeScript 7, the compiler refused the target outright:

class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ValidationError";
    Object.setPrototypeOf(this, new.target.prototype); // ES5-only fix
  }
}
console.log(new ValidationError("x") instanceof ValidationError);

Compiler output with –target es5:

error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.

So with ES2015 or later output you can drop the line. Keep it only if an older toolchain still emits ES5 code.

TypeScript Custom Error Cheat Sheet

This is what we tested above, in one place:

GoalCodeNeeds
Custom error classclass X extends Error + super(message)Any target ES2015+
Readable name in logsthis.name = "X" or new.target.nameNothing extra
Extra datareadonly status: numberNothing extra
Keep the original errorsuper(message, { cause })ES2022 lib, Node.js 16.9+
Group related errorsabstract class AppError + subclassesNothing extra
Route by kindswitch (err.code) on a string unionLiteral code types
Safe catch blocke instanceof Error or an e is X guardstrict (unknown catch)

Common Mistakes to Avoid With Custom Errors

These four bite teams most often. We reproduced each one:

class QuotaError extends Error {}   // forgot to set this.name

try {
  throw new QuotaError("Limit of 100 requests reached");
} catch (err) {
  console.log("1:", String(err));
}

try {
  throw "Payment failed";            // a string, not an Error
} catch (err) {
  console.log("2:", err instanceof Error, typeof err);
}

const e = new QuotaError("Limit of 100 requests reached");
console.log("3:", JSON.stringify(e));
console.log("4:", JSON.stringify({ name: e.name, message: e.message }));

Output:

1: Error: Limit of 100 requests reached
2: false string
3: {}
4: {"name":"Error","message":"Limit of 100 requests reached"}
  • No this.name: line 1 prints Error:, so the class name is lost in logs.
  • Throwing strings: line 2 shows false string, with no stack trace and no instanceof check.
  • Sending errors as JSON: JSON.stringify returns {} because name and message aren’t enumerable. Copy the fields first, like line 4.
  • Using this before super(): the compiler stops you, so always call super(message) first.

Turning an object into JSON safely is covered in converting objects to JSON in TypeScript.

Next Steps After Creating Custom Errors

Once your error classes exist, the next job is catching them well. Our tutorial on catching error types in TypeScript shows how to handle several error classes in one catch block.

We’d read these next:

Frequently asked questions

How do I create a custom error type in TypeScript?

Write a class that extends Error, call super(message) in the constructor and set this.name to the class name.

Why does instanceof fail on my custom error?

Your code is compiled to ES5. Target ES2015 or later, or add Object.setPrototypeOf(this, new.target.prototype) in the constructor.

How do I add a status code to a TypeScript error?

Declare a property such as readonly status: number on the class and set it in the constructor after super().

Why is the catch variable unknown in TypeScript?

Strict mode turns on useUnknownInCatchVariables, because any value can be thrown. Narrow it with instanceof or a type guard.

Should I use custom error classes or error codes?

Use both. Classes give you instanceof checks, and a code union gives you a clean switch across a whole hierarchy.

Where is the Error class documented?

See Error on MDN, including the cause option.