Conditionally catch a JavaScript exception based on type and properties,
What | Where |
---|---|
Discussion | https://github.com/bigeasy/rescue/issues/1 |
Documentation | https://bigeasy.github.io/rescue |
Source | https://github.com/bigeasy/rescue |
Issues | https://github.com/bigeasy/rescue/issues |
CI | https://travis-ci.org/bigeasy/rescue |
Coverage: | https://codecov.io/gh/bigeasy/rescue |
License: | MIT |
npm install rescue
A Pleading
Gentle user, you have stumbled upon a library that will seem to you to be a
silly little doodle of duplication, for what's wrong with the if/else
ladder
that we all know and hate?
{ try // Set widget preferences with the configuration in the `file`. this catch error if error instanceof SyntaxError // Syntax error, so we should give the user some context, then // throw the error. console throw error else if errorcode == 'ENOENT' // File not found, so create one with defaults. this // Unknown error, propagate. throw error }
With rescue
, you can say the same thing, with just about as much code.
{ try // Set widget preferences with the configuration in the `file`. this catch error // If rescue cannot match the error, it will propagate. }
So, what, pray tell, is wrong with me?
You see, dear user, over the years I have developed a fetish for 100% code coverage. Let's look at these examples again through the eyes of someone who wants to see green bars generated by Istanbul on every line of code.
{ try // Easy to test, just pass in the name of a good config file. this catch error if SyntaxError // Easy to test, just pass in the name of a bad config file. console throw error else if errorcode == 'ENOENT' // Easy to test, just pass in a missing file name. this
If you read the comments, you'll see that I want the default throw of the type
matching ladders in other languages. rescue
give me this.
But wait, there's more.
I've also gotten into the habit of wrapping errors in this library I created
called Destructible
, which I use to monitor the many async functions a
contemporary Node.js app spawns, and see that they all cancel and return when
shutdown time comes. If they don't shutdown, Destructible
will raise an
exception. This is an exception of exceptions, since more than one can fail to
shutdown.
Additionally, Destructible
will monitor these anonymous worker functions,
catch their exceptions and provide content in the form of a monitor name, so
that those terse stack traces whose only message is "socket hang up"
have some
context without resorting to using longjohn
in production.
Thus, nested exceptions, and deeply, deeply.
Rescue can search for an error in a nested heirarchy of errors and their causes.
Sometimes there is an excpetion expected, and if nothing else is in error, I can
recover from that one exception, and so I use rescue
to pluck it out of the
heirarchy, assert that is is the sole cause, and throw the specific exception to
the caller who can deal with it.
{ try try throw 'mischief' catch inner const outer = 'wrapper' outercauses = inner throw outer catch error // We can deal with a little mischief if that's all that's going on. }
So you see, rescue
will go searching for a "mischief"
error in a tree of
errors matching it if it is the only root cause. And by matching it (by the
message name this time) it will not be rethrown.
Without rescue
I'd have to implement this search in every catch block. My unit
tests would be way too intense.
You're Still Here?
Godness gracious, dear user, you're still here? Well, let's continue with a
definition of the one and only export from the Rescue module, rescue
.
Imagine that ?
, +', and
(?: )` mean what they mean in JavaScript regular
expressions, but instead of matching characters we're matching arguments.
rescue(error,(?:match:Array,(?:result|handler:Function)?)+)
You call rescue with the error you want to test, followed by one or more possible matches. Each match can be prefixed with an optional options object.
recscue(error, (?: [ only:Boolean?, (?: depth:Integer | range:Array )?, errorType:Function?, messageOrToString:String?, properties:Object?, aribraryTest:Function* ], (?: result | handler:Function )? )+)
In the above notation arguments in angle brackets are optional.
The basic structure of an incation of rescue is the error to rescue followed by one or more possible matches. The matche conditions are defined in an array along with the handler function to call if the action succeeds.
As we've seen, we can use rescue to ensure that an exception matches what we expect.
try {
config(JSON.parse(json))
} catch (error) {
rescue(error, [ SyntaxError ])
config(DEFAULT_JSON)
}
We can also use rescue
to return a value. The return value of rescue
is the
return value of the match's handler.
try {
return JSON.parse(json)
} catch (error) {
return rescue(error, [ SyntaxError ], () => DEFAULT_JSON)
}
We can make that simpler by just specifying a value to return instead of a handler function to call.
try return JSON catch error return
We do not need to specify an error type. We can simply specify a message we want to match.
try return true catch error return
It's hard to trust messages to stay consistent, especially if they include context information. You probably want to use a reguar expression to match the bits you know to be consistent.
try return JSON catch error return
Bad example, though. For JSON it is best to match SyntaxError
, which is what
we have been doing. Regular expressions are great for matching Node.js
Error.code
properties as we see next.
For many Node.js errors you can match the code
property.
try return await fs catch error return
You can also match properties by regular expression, which allows you to have some or conditions.
try return await fs catch error return
Function results can do useful work.
try return await fs catch error return await
You'll notice by now that rescue
works with async
/await
.