Home

There is no "Subclass Exception" in JavaScript

Gunar Gessner

Gunar Gessner

Apr 12, 2016

This is an old blog post. I'm keeping it alive for archiological reasons only. Check some recent stuff instead.

The JavaScript community has little agreement on how to Subclass Exceptions (a.k.a. Subclass Error, Extend Error, Custom Errors). At the root of the problem lies the fact that JS has Prototypical Inheritance, not Classical Inheritance.

I take for granted here that Classical Inheritance is problematic and one better avoid it. Also that `instanceof` "lies".

You can find resources on the subject here:

Custom Errors with Duck-typing

Let's say our code is expected to throw errors with messages the users should be aware of. Here is what I would do:

'use strict'

const MESSAGE_FOR_USER = Symbol()
const userError = function (message) {
  const error = Error(message)
  Object.defineProperty(error, MESSAGE_FOR_USER, { get() { return message } })
  // Omit frames above userError
  if (Error.captureStackTrace) Error.captureStackTrace(error, userError)
  return error
}
const getMessage = e => e[MESSAGE_FOR_USER] 

// -----

function foo() {
  throw userError('User be aware: Foo has been called.')
}

// Dummy function to examplify sending a message to the user
const send = msg => console.log(msg)

try { foo() }
catch (e) {
  const msg = getMessage(e)
  if (msg) { send(msg) } 
  else {
    // Unexpected exception
    console.error(e)
  }
}

// User be aware: Foo has been called.

Let's break it down...

# Symbol

We need to store the message in our error object. We can put it under a key named `message` or `userMessage`. However "message" is a common word and there's the possibility of conflicts with other code touching the same keys.

Symbol, a ES2016 feature, allow us to create unique identifiers. We can use a symbol to create a unique key in the object. For example

const UNIQUE_KEY = Symbol('anything, really')
const object = {}
object[UNIQUE_KEY] = 'Data'

.

It can be further simplified with ES2015's Computed Property Name, becoming

const UNIQUE_KEY = Symbol('anything, really')
const object = { [UNIQUE_KEY]: 'data' }

.

# Putting it under a Getter

Defining the key as a getter gives us confidence that no code is going to mutate that value.

One way to define a `getter` is with `defineProperty`, thus

  Object.defineProperty(error, MESSAGE_FOR_USER, {
    get() { return message },
  })

.

Side node: Getter

We could have set the `getter` at object creation,

const error = {
  get [MESSAGE_FOR_USER]() { return message }
}

, however in this case we can't because we want to build our object off of `Error`.

# Error.captureStackTrace

`userError()` creates an `Error` object to be used as base for our error. This object has a `stack` property (i.e. `error.stack`). Stack-trace information is useful so we can see where the error has been thrown.

Here `error.stack` contains information about the place where the error object was created. We don't want that. We only want the stack from where the object has been thrown. For example:

'use strict'

function e() {
  const error = Error()
  return error
}
throw e()

outputs

Error
    at e (code/subclassing-exception.js:4:17)               
    at Object.<anonymous> (code/subclassing-exception.js:7:7)
    at Module._compile (module.js:571:32)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:425:7)
    at startup (bootstrap_node.js:146:9)

The line that refers to `throw` has been pushed to second place. The first line points to `e()`. This is not such a big of a deal, as we could always just ignore that first line.

We could mutate `error.stack` as a string. However `Error` is implementation-specific and our code could break in different environments.

In `Node.js`, there's a helper function called `Error.captureStackTrace` which does exactly that for us. For example

'use strict'

function e() {
  const error = Error()
  Error.captureStackTrace(error, e)
  return error
}
throw e()

outputs

Error
    at Object.<anonymous> (/code/subclassing-exception.js:8:7)
    at Module._compile (module.js:571:32)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:425:7)
    at startup (bootstrap_node.js:146:9)
    at bootstrap_node.js:540:3

No reference to `e()`.

Spice it up

I like my JS with splashes of `lodash/fp` and `modules` (in `Node`). So what I would actually do is

'use strict'

const { get } = require('lodash/fp')

const MESSAGE_FOR_USER = Symbol('userError')

const userError = message => {
  const error = Error(message)
  Object.defineProperty(error, MESSAGE_FOR_USER, { get() { return message } })
  // Omit frames above userError
  if (Error.captureStackTrace) Error.captureStackTrace(error, userError)
  return error
}

const getMessage = get(MESSAGE_FOR_USER)

Object.assign(userError, {
  getMessage,
})

module.exports = userError
'use strict'

const userError = require('./userError')

function foo() {
  throw userError('User be aware: Foo has been called.')
}

// Dummy function to examplify sending a message to the user
const send = msg => console.log(msg)

try { foo() }
catch (e) {
  const msg = userError.getMessage(e)
  if (msg) { send(msg) } 
  else {
    // Unexpected exception
    console.error(e)
  }
}

// User be aware: Foo has been called.

Thank you.


Sign up for the newsletter


Read other stuff