15

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: https://esdiscuss.org/topic/creating-your-own-errors

3
  • 1
    I think your second method (setting custom error codes) is more common ways to handle this, as node.js also uses this method. Though it might have one downside to it, i.e. Your error instance cant have any custom methods. If you are concerned with just logging the errors, then i think setting custom error code would be a way to go. Lets see what others have to say about it. Commented Nov 17, 2017 at 4:15
  • 1
    Thanks. Another example I came across is in the MDN docs where it shows (though not necessarily advocating for) setting name.
    – Aaronius
    Commented Nov 17, 2017 at 4:24
  • 4
    You can create several classes inherited from Error and then in catch block check is e instanceof FirstCustomError Commented Nov 17, 2017 at 5:59

1 Answer 1

6

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.

Failed operations

For failed operations, there's usually just one datapoint: the name of the operation. I use this format:

operation + space + "failed"
// e.g.
"UserPreferences.save failed"

As a rule, operation is the name of the routine exactly as it was invoked:

try {
  await API.updateUserProfile(newData)
  
} catch( error ) {
  throw new Error('API.updateUserProfile failed')
}

Error messages are not prose

Error messages look like user-facing text, and that usually activates the prose-writing parts of our brains. That means you're going to ask yourself questions like:

  • Should I use sentence case? (e.g. start with a capital, end with punctuation)
  • Do I need a comma here? Or should it be a semicolon?
  • Is it "e-mail" or "email"? And what about "ZIP code"?
  • Should my "bad phone number" message include the proper format?

Each of these questions, and countless others like them, are signposts marking the wrong road.

An error message is not a tweet, it's not a note to the next developer, it's not a tutorial for novice devs, and it's not the help message you'll show users. An error message is a dev-readable error code, period. The only reason I don't recommend using numeric constants (like Windows does) is that nobody will maintain a separate document that maps each error number to a human-readable explanation.

So every time the creative author inside you pipes up and offers to help you wordsmith the "perfect" error message, kick that author out of the room and lock the door. Aggressively keep out anything that might fall under the umbrella of "editorial style". If two reasonable people could answer the question differently, the solution is not to pick one, it's to do neither.

So:

  • no punctuation, period (ha!)
  • 100% lowercase, except where topic or operation must be mixed-case to reflect the actual tokens

This isn't the only way to keep your errors straight, but this set of conventions does make it easy to do three important things:

  • write new error-throwing code without having to think very hard;
  • react intelligently to exceptions; and
  • locate the sources of most errors that can be thrown.

What should you throw?

We've been discussing conventions for error messages, but that ignores the question of what kind of thing to throw. You can throw anything, but what should you throw?

I think you should only throw instances of Error, or, when appropriate, one of its sub-classes (including your own custom sub-classes). This is true even if you disagree with all my advice about error messages.

The two biggest reasons are that native browser code and third-party code throw real Errors, so you must do the same if you wish to handle them all with a single codepath & toolset, and because the Error class does some helpful things (like capturing a stacktrace).

(Spoiler: of the built-in sub-classes, the ones I suspect you'll have the most use for are TypeError, SyntaxError, and RangeError.)

Regardless of which variety you throw, I think there is exactly one way to throw an Error correctly:

throw new Error( message )

If you want to attach arbitrary data to the error before throwing it, you can do that:

let myError = new RangeError('forbidden domainName')
myError.badDomainName = options.domain
throw myError

If you find yourself doing this a lot, it's a sign you could use a custom sub-class that takes extra parameters and wires them into the appropriate places automatically.

1
  • Really nice explanation, I personally prefer the prose approach (mostly for small projects) to be very specific in why it's failing. It's great to have another perspective anyway.
    – itmarck
    Commented Mar 6 at 8:00

Not the answer you're looking for? Browse other questions tagged or ask your own question.