Compared to languages like Java and Python, Javascript/Typescript’s error handling features have always felt pretty half-baked to me.

Creating custom error types requires a bit of prototype fiddling, and handling errors in catch blocks feels much more primitive than it should be.

With this post I just wanted to share the snippets of code I’ve brought to most of the Typescript projects I’ve worked on, to solve these issues.

🏃🏼 tl;dr: see this Gist for a better Error class and two functions to help tidy up your catch blocks

A More Helpful Error Class

As you’ll know if you’ve ever wanted to create an Error subclass, there’s a bit of boilerplate needed in the constructor to do it “properly”.

We need to set the instance’s name property to the name of our error class.

As well, to make Typescript happy with our class, we need to explicitly set its prototype.

export class MyError extends Error {
  constructor(message: string) {
    super(message);
		this.name = "MyError";
		Object.setPrototypeOf(this, MyError.prototype);
  }
}

It’s possible to create a base error class that abstracts all of this boilerplate, meaning its subclasses don’t need to bother, like so —

export class ExtError extends Error {
  constructor(message: string) {
    super(message);
    Object.defineProperty(this, "name", { value: new.target.name });
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

This base error class just covers the basic issues I’ve outlined above.

I’d use a library like ts-custom-error if you want to add this to a serious codebase — it does things a bit more officially, and also handles setting the stack property properly.

Error Causes

TC39, the people that maintain the Javascript standard, recently implemented their proposal to add a cause property to the base Error type.

It’s been supported in Node since 16.9, and is implemented in every major browser, according to MDN.

This means you’ll be able to give a cause property to Error's constructor, like so —

try {
	somethingBad();
} catch (err) {
	throw new Error("something bad happened!", { cause: err });
}

When running in Node.js at least, we get a nice indented dump of the cause’s error stack.

% npx ts-node index.ts
/Users/edward.gargan/personal/ts-errors/index.ts:8
  throw new Error("something bad happened!", { cause: err });
        ^
Error: something bad happened!
    at Object.<anonymous> (/Users/edward.gargan/personal/ts-errors/index.ts:8:9)
    at Module._compile (node:internal/modules/cjs/loader:1165:14)
    ...
  [cause]: Error: ENOENT: no such file or directory, open 'doesnt-exist.txt'
      at Object.openSync (node:fs:590:3)
      at readFileSync (node:fs:458:35)
      at Object.<anonymous> (/Users/edward.gargan/personal/ts-errors/index.ts:5:28)
     ...

Adding Causes to Subclasses

To attach a cause to subclasses of Error, you’d need to make sure to add a cause parameter to your subclass’ constructor.

Or, something I’ve found to be a bit cleaner, is to add a method to the base error class that sets the cause member after construction.

export class ExtError extends Error {
  constructor(message: string) {
    super(message);
    Object.defineProperty(this, "name", { value: new.target.name });
    Object.setPrototypeOf(this, new.target.prototype);
  }

  from(cause: unknown): ExtError {
    this.cause = cause;
    return this;
  }
}

I’ve not found any issues with this approach versus passing a cause to the super constructor — assigning cause late doesn’t change how it’s handled by Node when it’s dumped out.

Note that you’ll need to set your TS config’s target to at least es2022 to stop it complaining about this.cause = cause.

More Elegantly Handling Errors in catch Blocks

Javascript’s catch block is pretty boring. You catch an error, you give it a name, that’s all it lets you do.

Whereas in Python, for example, you can write many catch blocks, each dealing with one or a few specific error types.

try:
  data = getData(filepath)
except IOError as err:
  log.warn("file could not be loaded", err);
except (ValueError, MissingValueError) as err:
  log.warn("file is corrupted", err);

We can achieve the same in Javascript with slightly clunkier code, using instanceof checks, as follows.

try {
  data = getData(filepath);
} catch (err) {
	if (err instanceof FileError) {
	  log.warn("file could not be loaded", err);
	} else if (err instanceof TypeError | err instanceof MissingValueError) {
	  log.warn("file is corrupted", err);
	}
}

Another key feature of Python’s (and many other languages’) error handling is that errors “bubble up” if they’re not explicitly caught.

In Javascript, we have to manually re-throw our errors at the end of the every catch block to get the same behaviour.

try {
  data = getData(filepath);
} catch (err) {
	...
	} else {
		throw err;
	}
}

These two shortcomings led me to write these little matchErr and matchErrOrThrow functions that, for me, encourage safer and more thorough error handling.

Here’s how they look —

try {
  data = getData(filepath)
} catch (err) {
	if (matchErr(err, FileError)) {
	  log.warn("file could not be loaded", err);
	} else if (matchErrOrThrow(err, TypeError, MissingValueError)) {
	  log.warn("file is corrupted", err);
	}
}

As with instanceof checks, these functions will narrow err according to the given error types. E.g. within that else if block, err's type will be TypeError | MissingValueError.

See this Gist for the implementation of these functions.

Throwing vs. Returning Error States

Having spent a bit of time with Rust, I quickly fell in love with its Result type, a container for either an error type or a “successful” value type.

In either case, this Result is returned from the function. Errors are never “thrown” and “caught” as they are in Javascript.

Returning errors like this means every path through a function — and so your program — is type safe.

Using languages that throw errors, it can be difficult to cover all of the errors states that a function can produce, as you won’t be told if you haven’t handled a particular error (except in Java, which does check that you’ve declared the exceptions that a function throws, and that you’ve handled all of them when you call it).

There are plenty of libraries out there that give you a Result type for Typescript code, but without first-class language support for it like Rust has, it’s just not worth it, in my opinion.

Javascript’s error handling features are definitely lacking, but I’d rather use them than fight against the language and force my own patterns onto it.