Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 533
Aaronius Avatar asked Nov 17 '17 03:11

Aaronius


People also ask

How do you show errors in JavaScript?

Errors in JavaScript can be displayed without the use of alert boxes but using the alert box is the traditional way to do that. We can show errors with two methods without using the alert box. Syntax: node.

How do you return an error in JavaScript?

To return errors from functions as promises, there are generally two ways to produce rejected promises: The reject function of your Promise library. Throwing an exception in a then callback or returning a rejected promise from it will reject the resulting promise.

How will you handle the error JavaScript?

JavaScript provides error-handling mechanism to catch runtime errors using try-catch-finally block, similar to other languages like Java or C#. try: wrap suspicious code that may throw an error in try block. catch: write code to do something in catch block when an error occurs.

Is there a better way to handle errors in JavaScript?

As far as error handling, this is pretty bad. A fail-silent strategy will leave you pining for better error handling. JavaScript offers a more elegant way of dealing with exceptions. Time to investigate an ugly handler. I will skip the part that gets tight-coupled to the DOM. There is no difference here from the bad handler you saw.

What are the three types of errors in programming?

There are three types of errors in programming: (a) Syntax Errors, (b) Runtime Errors, and (c) Logical Errors. Syntax errors, also called parsing errors, occur at compile time in traditional programming languages and at interpret time in JavaScript. For example, the following line causes a syntax error because it is missing a closing parenthesis.

What is a syntax error in JavaScript?

Syntax errors, also called parsing errors, occur at compile time in traditional programming languages and at interpret time in JavaScript. For example, the following line causes a syntax error because it is missing a closing parenthesis.

How to handle errors in a program?

Returning an error code is another valid option for handling errors. We can return an error code to let users and other parts of our program know that an error is encountered. To return errors, we can set the value of a status variable, we can return the status code, or we can throw an exception with the status as the value.


1 Answers

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.

like image 156
Tom Avatar answered Sep 17 '22 23:09

Tom