What's the Best Way to Provide an Error Code on a Javascript Error?

I have a library that throws errors:

throw new Error('The connection timed out waiting for a response')

It can throw errors for several different reasons and it's hard for users to programmatically handle the error in different ways without switching on error.message, which is less than optimal since the message isn't really intended for programmatic decisioning. I know a lot of people subclass Error, but it seems overboard. Instead, I'm considering (a) overriding error.name with a custom name:

const error = new Error('The connection timed out waiting for a response');
error.name = 'ConnectionTimeout';
throw error;

or (b) setting error.code (not a standard property):

const error = new Error('The connection timed out waiting for a response');
error.code = 'ConnectionTimeout';
throw error;

Is there a preferred approach? Are either of these approaches frowned upon? Here's the closest conversation I could find regarding the subject, but it seems inconclusive and maybe out of date with new conventions:

3

1 Answer

This is a substantially augmented partial reprint of a much larger answer (of my own), the majority of which is not relevant. But I suspect that what you're looking for is:

Error messages for sane people

Consider this bad (but common) guard code:

function add( x, y ) {
  if(typeof x !== 'number')
    throw new Error(x + ' is not a number')
  
  if(typeof y !== 'number')
    throw new Error(y + ' is not a number')
  
  return x + y
}

Every time add is called with a different non-numeric x, the error.message will be different:

add('a', 1)
//> 'a is not a number'

add({ species: 'dog', name: 'Fido' }, 1) 
//> '[object Object] is not a number'

In both cases I've done the same bad thing: provided an unacceptable value for x. But the error messages are different! That makes it unnecessarily hard to group those cases together at runtime. My example even makes it impossible to tell whether it's the value of x or y that offends!

These troubles apply pretty generally to the errors you'll receive from native and library code. My advice is to not repeat them in your own code if you can avoid it.

The simplest remedy I've found is just to always use static strings for error messages, and put some thought into establishing conventions for yourself. Here's what I do.

Most of the exceptions I've thrown fall into 2 categories:

  • some value I wish to use is objectionable
  • some operation I attempted has failed

I've found that each category is well-served by having its own rules.

Objectionable values

In the first case, the relevant info is:

  • which datapoint is bad; I call this the topic
  • why it is bad, in one word; I call this the objection

All error messages related to objectionable values ought to include both datapoints, and in a manner that is consistent enough to facilitate flow-control (this is your concern, I think) while remaining understandable by a human. And ideally you should be able to grep the codebase for the literal message to find every place that can throw the error (this helps enormously with maintenance).

Based on those guidelines, here is how I construct error messages:

objection + space + topic
// e.g.
"unknown planetName"

There is usually a discrete set of objections:

  • missing: value was not supplied
  • unknown: could not find value in DB & other unresolvable-key issues
  • unavailable: value is already taken (e.g. username)
  • forbidden: sometimes specific values are off-limits despite being otherwise fine (e.g. no user may have username "root")
  • non-string: value must be a String, and is not
  • non-numeric: value must be a Number, and is not
  • non-date: value must be a Date, and is not
  • ... additional "non-[specific-type]" for the basic JS types (although I've never needed non-null or non-undefined, but those would be the objections)
  • invalid: heavily over-used by dev community; treat as option of last resort; reserved exclusively for values that are syntactically unacceptable (e.g. zipCode = '__!!@')

I supplement individual apps with more specialized objections as needed, but this set has covered the majority of my needs.

The topic is almost always the literal variable name as it appears within the code block that threw. To assist with debugging, I think it is very important not to transform the variable name in any way (such as changing the lettercase).

This system yields error messages like these:

'missing lastName'
'unknown userId'
'unavailable player_color'
'forbidden emailAddress'
'non-numeric x'

These messages are always two "words" separated by a single space, thus: let [objection, topic] = error.message.split(' '), and you can then do stuff like figure out which form field to set to an error state.

Make your guard-clause errors scrupulously honest

Now that you have a set of labels that allow you to report problems articulately, you have to be honest, or those labels will become worthless.

Don't do this:

function add( x, y ) {
  if(x === undefined)
    throw new Error('non-numeric x') // bad!
}

Yes, it's true that undefined is not a numeric value, but the guard clause doesn't evaluate whether x is a number. This skips a logical step, and as smart humans it's very easy to skip it. But if you do, many of your errors will be lies, and thus useless.

It actually takes a lot of work to make a function capable of expressing every nuance correctly. If I wanted to go "whole-hog", I'd have to do something like this:

function add( x, y ) {
  if(x === undefined) throw new Error('missing x')
  if(typeof x !== 'number') throw new Error('non-numeric x')
}

For a more interesting value type, like a Social Security Number or an email address, you'll need an extra guard clause or two, if you want to make your function differentiate between every possibly failure scenario. That is usually overkill.

You can usually get away with just one guard clause that confirms that the value is the one acceptable thing, rather than going through all the effort of determining which kind of unacceptable thing you've received. The difference between those two is where lazy thinking will result in dishonest error messages.

In my example above, it's okay if we just do typeof x !== 'number'; we probably aren't losing anything valuable if this function is unable to distinguish between undefined and any other non-numeric. In some cases you may care; then, go "whole-hog" and narrow the funnel one step at a time.

But since you usually won't be performing every test, you must make sure the error message you select is a precise and accurate reflection of the one or two tests you do perform.

Alexander Ross

Alexander Ross

Gaming, Esports & Interactive Media Writer

Alexander Ross has covered the video game industry for a decade, writing deep dives on game design, esports tournaments, VR developments, and gaming culture.