M Logo
๐Ÿ“ TypeScript from Zero ยท Part 30 of 50

TypeScript Error Handling: try/catch with unknown, Custom Errors and Error.cause

November 6, 2026

Welcome to episode 30 of TypeScript from Zero, a series for complete beginners (no JavaScript needed). Welcome back to TypeScript from zero! Things go wrong: files are missing, input is bad. Today: how to handle errors, and how to make our own. We continue in the tasks project, with a new file called errors.ts. Every code block below is the exact code and output from the video, ready to copy.

Setup

We start in the terminal and open the project in VS Code:

bash
cd tasks
touch src/errors.ts
code .

1. try catch

A function that can fail uses throw. Parse port turns text into a number, and throws an error if that is not possible. Around the call we put try and catch. The code in try runs until something is thrown, and then catch takes over. So the first call prints eight thousand eighty, and the second one lands in the catch block.

src/errors.ts:

ts
function parsePort(text: string): number {
  const n = Number(text);
  if (Number.isNaN(n)) {
    throw new Error("not a number: " + text);
  }
  return n;
}
try {
  console.log(parsePort("8080"));
  console.log(parsePort("abc"));
} catch (err) {
  console.log("Caught:", err.message);
}
text
$ node src/errors.ts
8080
Caught: not a number: abc

2. unknown

But tsc is not happy with err dot message. In a catch block, the error is of type unknown, because in JavaScript anything can be thrown, even a number or a text. TS18046. The fix is to check first: if err is an instance of Error, then it has a message.

text
$ npx tsc --noEmit
src/errors.ts:12:26 - error TS18046: 'err' is of type 'unknown'.

12   console.log("Caught:", err.message);
                            ~~~

Found 1 error in src/errors.ts:12

3. Custom error

Our own error is a class that extends Error. A task error also carries the id of the task. We call super with the message, and set the name. Now a function can throw it, and the catch block can ask: is this a task error? Only then it may read the task id.

src/errors.ts:

ts
function parsePort(text: string): number {
  const n = Number(text);
  if (Number.isNaN(n)) {
    throw new Error("not a number: " + text);
  }
  return n;
}
try {
  console.log(parsePort("8080"));
  console.log(parsePort("abc"));
} catch (err) {
  if (err instanceof Error) {
    console.log("Caught:", err.message);
  }
}
class TaskError extends Error {
  readonly taskId: number;
  constructor(message: string, taskId: number, options?: ErrorOptions) {
    super(message, options);
    this.name = "TaskError";
    this.taskId = taskId;
  }
}
function findTask(id: number): never {
  throw new TaskError("task not found", id);
}
try {
  findTask(7);
} catch (err) {
  if (err instanceof TaskError) {
    console.log(err.name, err.taskId, err.message);
  }
}
text
$ node src/errors.ts
8080
Caught: not a number: abc
TaskError 7 task not found

4. cause

Often an error happens because of another error. The second argument of the error constructor has a field called cause, and it keeps the original error inside the new one. Here read id catches the first error, and throws a task error that remembers why. Nothing gets lost on the way.

src/errors.ts:

ts
function parsePort(text: string): number {
  const n = Number(text);
  if (Number.isNaN(n)) {
    throw new Error("not a number: " + text);
  }
  return n;
}
try {
  console.log(parsePort("8080"));
  console.log(parsePort("abc"));
} catch (err) {
  if (err instanceof Error) {
    console.log("Caught:", err.message);
  }
}
class TaskError extends Error {
  readonly taskId: number;
  constructor(message: string, taskId: number, options?: ErrorOptions) {
    super(message, options);
    this.name = "TaskError";
    this.taskId = taskId;
  }
}
function findTask(id: number): never {
  throw new TaskError("task not found", id);
}
try {
  findTask(7);
} catch (err) {
  if (err instanceof TaskError) {
    console.log(err.name, err.taskId, err.message);
  }
}
function readId(text: string): number {
  try {
    return parsePort(text);
  } catch (err) {
    throw new TaskError("bad id", 0, { cause: err });
  }
}
try {
  readId("x");
} catch (err) {
  if (err instanceof TaskError) {
    console.log(err.message, "because:", (err.cause as Error).message);
  }
}
text
$ node src/errors.ts
8080
Caught: not a number: abc
TaskError 7 task not found
bad id because: not a number: x

Recap

Today: throw, try and catch, the unknown error in catch and instance of, custom error classes, and the cause. Next episode: transforming data with map, filter and reduce.

ยฉ 2026 All Rights Reserved By Mahmudul Alam