Typesafe error handling in Typescript

When using try/catch, Typescript will infer the thrown type to be unknown:

try {
  // do some stuff
} catch (thrown) {
  //     ^? unknown
}

Normally, you'd expect an Error to be thrown, but Javascript lets you throw anything:

throw { type: "not an error" };

Inferring as unknown is a nice reminder of that. Thanks Typescript!

But handling unknown is annoying. You can't access things like message or stack that you'd expect from an Error since they might not be there.

try {
  // do some stuff
} catch (thrown) {
  console.error(thrown.message);
  //                   ^^^^^^^
  // Typecheck error: `unknown` does not have property `message`
}

Luckily, I stumbled on a post by Kent C. Dodds for typesafe error messages. But, I didn't want just the error message. I wanted the error itself. That way I could format the stacktrace or filter the error based on its name.

Here's my way of getting typesafety while handling errors. First define the asError utility:

let asError = (thrown: unknown): Error => {
  if (thrown instanceof Error) return thrown;
  try {
    return new Error(JSON.stringify(thrown));
  } catch {
    // fallback in case there's an error stringifying.
    // for example, due to circular references.
    return new Error(String(thrown));
  }
};

Then use asError to narrow the thrown type to Error:

try {
  // do some stuff
} catch (thrown) {
  //     ^? unknown
  let error = asError(thrown);
  //  ^? Error

  // access `name`, `message`, and `stack`
  console.error(`${error.name}: ${error.message}`);
  if (error.stack) console.error(error.stack);
}

This coercion of unknown to Error is blunt, so if you need to handle non-Errors more delicately, feel free to modify asError. I stick to using Errors whenever I'm throwing, but need a small patch like asError to handle the rare non-Error being thrown.

Custom errors

If you have a custom error type that is more specific than Error, you can further narrow the type with a typeguard or instanceof check:

class CustomError extends Error {
  data: Record<string, unknown>;
  constructor(message: string, data: Record<string, unknown>) {
    super(message);
    this.data = data;
  }
}

try {
  // do some stuff
} catch (thrown) {
  let error = asError(thrown);
  if (error instanceof CustomError) {
    console.error(error.data);
    //            ^? CustomError
    return;
  }
  throw thrown;
}