Custom Errors in JavaScript

2017-03-26

There is no “Subclass Exception” in JavaScript

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:

index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
'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

symbol.js
1
2
3
const UNIQUE_KEY = Symbol('anything, really')
const object = {}
object[UNIQUE_KEY] = 'Data'

.

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

symbolES2015.js
1
2
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

getter.js
1
2
3
Object.defineProperty(error, MESSAGE_FOR_USER, {
get() { return message },
})

.

Side node: Getter

We could have set the getter at object creation,

getterAtCreation.js
1
2
3
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:

stack-trace-cluttered.js
1
2
3
4
5
6
7
'use strict'
function e() {
const error = Error()
return error
}
throw e()
outputs
1
2
3
4
5
6
7
8
9
10
11
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

stack-trace-clean.js
1
2
3
4
5
6
7
8
'use strict'
function e() {
const error = Error()
Error.captureStackTrace(error, e)
return error
}
throw e()

outputs

1
2
3
4
5
6
7
8
9
10
11
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

userError.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'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
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'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.


Changelog

1
2017-03-29 Updated code to use `userError()` instead of `userError.of()`.

Comments: