A standard Exception class

In this post, when I say "error", I'm referring to a bug that was found in runtime, and when I say "exception", I'm referring to an operation that failed to complete.

Javascript does not have any standard way of dealing with exceptions - this splinters the community with different packages using different solutions to this problem.

For example, at work, I use an ldap-js npm package. Many of their exposed functions can cause a number of different exceptions to be thrown. These exceptions are all subclasses of Error, and are all exported together (as can be seen in their documentation here).

I also use node, and node's fs module will throw different exceptions by just throwing an instance of Error(), and attaching custom properties to it. For example, a file-not-found error can be distinguished from other errors by checking if the error instance has a code property that equals "ENOENT".

I propose we design a standard way to throw exceptions. The solution I'll propose here would be to add a new Exception class that inherits from Error

Here's an example Exception implementation:

class Exception extends Error {
  constructor(code, message = code) {
    super(message)
    this.code = code
    this.name = this.constructor.name
  }
}

Exampe usage:

function getResource() {
  if (resource) return resource
  throw new Exception('NotFound', 'Resource was not found.')
}

function getResourceOrDefault(defaultValue = null) {
  try {
    return getResource()
  } catch (ex) {
    if (!(ex instanceof Exception)) throw ex
    if (ex.code !== 'NotFound') throw ex
    return defaultValue
  }
}

Note that in the example implementation of Exception I set this.name to this.constructor.name. This makes inheriting Exception much easier, as a subclass will automatically receive the class name as the name property. Inheritance is important to preserve compatibility with existing exception solutions, among other reasons.

// A package that used to make all of their exception inherit from Error ...
class CustomError extends Error {
  constructor(message) {
    super(message)
    this.name = 'CustomError'
  }
}

// ... can now just have their exceptions inherit from Exception,
// without making any breaking changes
class CustomError extends Error {
  constructor(message) {
    super('CustomError', message)
    // this.name will automatically be set to 'CustomError'
  }
}

Having a standard Exception solution can be beneficial for a number of reasons. One of which is that it'll be easier to make helper exception functions, because everyone follows the same standard. For example:

function ignoreExceptionOfType(code, fn) {
  return (...args) => {
    try {
      return fn(...args)
    } catch (ex) {
      if (ex instanceof Exception && ex.code === code) {
        return null
      }
      throw ex
    }
  }
}

A helper function like this wouldn't be very possible if people keep using their own custom solutions for exception handling.

you can't rely on subclassed exceptions in common-case they need be message-passed over wire (e.g. swagger, web-workers, etc).

the only way to standardize exceptions in a fullstack/message-passing environment is treating them as plain json-objects with some standardized property like statusCode/code/type/etc.

Then, I guess it's good that the proposed standard Exception class requires people to pick a unique exception code for their class, even if they also use subclasses to distinguish one exception from another.

if it has an easy-to-check, unique exception-code, why would anyone go through the error-prone hassle of checking for a subclass?

The purpose is to allow library owners to transition to this Exception class, without making a breaking change. For example, code like this exists in the wild everywhere:

// thirdPartyLibrary.js

export class NotFoundException extends Error {}

export function fetchResource(name) {
  if (!hasResource(name)) {
    throw new NotFoundException(`Resource ${name} not found.`)
  }
  return getResource(name)
}

// userland.js

import { NotFoundException, fetchResource } from 'thirdPartyLibrary'

export function myFunction() {
  // ...
  try {
    return fetchResource()
  } catch (ex) {
    if (ex instanceof NotFoundException) {
      return null
    }
    throw ex
  }
}

If thirdPartyLibrary wants to start supporting this new Exception standard, all they would have to do is change their class definition to this:

// thirdPartyLibrary.js

export class NotFoundException extends Exception {
  constructor(message) {
    super('NotFound', message)
  }
}

Old users of this library won't have to update their code (they can keep using instanceof). New users can reap the benefits from the fact that this is now in a standard format, and just use the "code" property. Internally, they don't have to update their own codebase. No breaking changes. Everyone's happy.

There's other reasons to want to subclass Exception. For example, it's not uncommon for networking libraries to provide a custom HTTPException class, where you can provide an HTTP status code during construction. If the exception goes uncaught, then that status code will be used in the message that's returned to the end-user. Note that in this use-case, there would be just one subclass of Exception for a wide variety of exceptions, so one wouldn't be able to pick out an individual exception using instanceof - they can just pick out the fact that it is an HTTPException with instanceof, then they can infer what the shape of the instance will be (with its additional properties, such as statusCode), because those extra properties will always be present on this subclass of Exception (useful for typescript). This certainly isn't the only way to achieve this, but I don't see any reason why it should be discouraged.

This sounds more like a platform concern than a spec concern. The DOM already uses DOMException broadly, and Node just tacks on expando properties to a new Error.

There really isn't any single well-defined pattern for exceptions - each host does its own thing.

I think you just laid out the arguments of why I'm in favor of this idea. Because "There really isn't any single well-defined pattern for exceptions - each host does its own thing". The DOM, node, and third-party libraries are all using their own solutions to this problem, and it would be nice if we unified them.

Though, maybe there isn't a big interest to unify them :man_shrugging:

I've been preparing a different proposal that required a unified Exception class, such as this, but thought I would present this piece first, by itself, because having a unified Exception class seemed to be valuable in-and-of-itself.

But, I admit, as the language currently stands, there wouldn't be a ton of value in unifying them. It would be more useful if we also wanted to add language-level features that distinguished one exception type from another. I do think it makese the language easier to use - anyone who's less familiar with Javascript who wants to throw an exception now knows exactly what to do after a quick google search. Currently, they will be presented with multiple similar methods, some of which are a little difficult to pull off (there's a lot of talk out there on how to properly inherit the Error class).

If this was a pattern that had always existed, I agree it would probably make the language easier to use - but at this point, the language can't impose that pattern on the ecosystem (on userland or on engines) and the existing forms in the language can't change (due to web compat) so it seems like adding this now would make things less clear.

That's why I designed the proposed solution the way I did - backwards compatibility is very important to me. The Exception class I'm proposing above is designed to be distinguished by an error code, but it also inherits off of Error and can be subclassed to keep backwards compatibility.

So, for example, DOMExceptions could inherit from Exception instead of Error, and each DOMException could also provide an error code - this is not a breaking change, no user would have to modify their code as a result of this.

Node could start throwing instances of Exception instead of Error. They already provide an error code as a "code" property, so nothing needs to change there. They can still tack on their custom properties to Exception. This is also not a breaking change, and the end-user doesn't have to change a line of code if Node does this.

Once this is done, both of these platforms will now be using the same unified Exception class (in slightly different ways). The end-user can now just use the provided error code to distinguish the exceptions from each other.

Changing DOMException’s inheritance chain is unlikely to be backwards compatible. Code in the wild iterates over the prototype chain, and ive seen code that counts the number of hops to Object.prototype, for example.

Oh - didn't realize that counted as a breaking change - didn't know there was code that did that kind of thing. That's a shame. It's probably better not to promote this standard then.