UNPKG

nani

Version:

Better error handling for Node

1,117 lines (881 loc) 35.8 kB
# Nani Better Error handling for Node, inspired in part by [VError](https://www.npmjs.com/package/verror). Includes a base class for custom error types, along with utilities for identifying your errors based on their class hierarchy *without* relying on `instanceof`. Makes it easier to tell what happened when things explode. :) ![Screenshot](https://github.com/sripberger/nani/raw/master/nani.png) ## Recent Changes This section will include detailed explanation for only the most recent major or minor releases. You can view a full changelog [here](./CHANGELOG.md). ### `usedDefaultMessage` property As of `v3.2.0`, the `NaniError` constructor sets the `usedDefaultMessage` property, depending on whether or not a shortMessage was provided to it. This is intended to be used for unit testing, allowing you to easily write tests that ensure an error was thrown with a default message, without actually caring what the default message *is*. ### Static `prefix` property As of `v3.1.0`, you can specify a static `prefix` property on your error classes. This string will be prepended to the full messages of all errors of that type. It will not affect the `shortMessage` property, however. The prefix, if present, is separated from the short message the same way cause messages are separated from one another, with a colon surrounded by spaces. ### `is` Function Signature (BREAKING CHANGE) As of `v3.0.0`, the `is` function's positional arguments have been reversed. The original signature was chosen because to me, it seemed to read closer to English, but it isn't really idiomatic with JS in general and ultimately is more likely to confuse people familiar with libraries like `lodash`, which always put the primary operand of an operation on the left side. To see if a collection includes a value with lodash, for example, you would do this: ```js _.includes(collection, value); ``` `is` is also intended mostly as a drop-in replacement for the `instanceof` operator, so even within the JS standard itself, the original signature is just backwards for no good reason. While this might not seem like a big deal, it is something that is better changed sooner than later, if it's going to be changed at all. As such, I've changed it now, and will be deprecating all versions previous to v3.0.0. Since the package originally published with this name (any 1.x release) is something completely different, it's hard to tell based on downloads alone how many dependents this change will affect. I don't plan on continuing to add any features for the 2.x releases, but if you end up needing one-- and don't want to switch all of your `is` usages over just yet-- feel free to file an issue on GitHub and I'll take a look. ## The Cause Chain Users of VError will be familiar with cause message chains, but for those that are not, the idea is that errors can be given a `cause` when constructed. A cause's message will be chained to the end of the wrapping error's message, making it easy for a human programmer to get the full story at a glance. Nani provides the `NaniError` class for this purpose, among other features: ```js const { NaniError } = require('nani'); try { JSON.parse('invalid JSON'); } catch (err) { throw new NaniError({ shortMessage: 'Parsing failed', cause: err }); } /* Running the above code produces something like the following: NaniError: Parsing failed : Unexpected token i in JSON at position 0 at Object.<anonymous> (/home/sripberger/projects/personal/nani/omg.js:6:8) at Module._compile (module.js:635:30) at Object.Module._extensions..js (module.js:646:10) at Module.load (module.js:554:32) at tryModuleLoad (module.js:497:12) at Function.Module._load (module.js:489:3) at Function.Module.runMain (module.js:676:10) at startup (bootstrap_node.js:187:16) at bootstrap_node.js:608:3 */ ``` The cause is of course optional, and defaults to `null`. If the `shortMessage` is omitted, a generic default one will be used instead. The `cause`, `shortMessage`, and full `message` will be available as properties on the created error. ## Hiding Cause Messages Occasionally, you will want an error's message to simply match its shortMessage, without appending the cause's message. To do so, set the `hideCauseMessage` option to `true`. This will cause the `NaniError` to behave somewhat like a `WError`: ```js const { NaniError } = require('nani'); try { JSON.parse('invalid JSON'); } catch (err) { throw new NaniError({ shortMessage: 'Parsing failed', cause: err, hideCauseMessage: true }); } /* Running the above code produces something like the following: NaniError: Parsing failed at Object.<anonymous> (/home/sripberger/projects/personal/nani/omg.js:6:8) at Module._compile (module.js:635:30) at Object.Module._extensions..js (module.js:646:10) at Module.load (module.js:554:32) at tryModuleLoad (module.js:497:12) at Function.Module._load (module.js:489:3) at Function.Module.runMain (module.js:676:10) at startup (bootstrap_node.js:187:16) at bootstrap_node.js:608:3 */ ``` This can be useful for public-facing API's where you don't want to clutter the error messages your users see with internal stuff they don't care about, while still preserving the underlying cause chain for your own debugging purposes. ## Viewing the Full Stack Node's default uncaught exception handler simply prints out the `stack` property of the exception to `stderr` and exits. This, of course, doesn't tell you anything about the stack traces in the cause chain. To view this, Nani provides the `getFullStack` function: ```js const { NaniError, getFullStack } = require('nani'); // Setting our own uncaught exception handler. process.on('uncaughtException', (err) => { // Print full stack to sterr and exit. console.error(getFullStack(naniErr)); process.exit(1); }); try { JSON.parse('invalid JSON'); } catch (err) { throw new NaniError({ shortMessage: 'Parsing failed', cause: err }); } /* Running the above code produces something like the following: NaniError: Parsing failed : Unexpected token i in JSON at position 0 at Object.<anonymous> (/home/sripberger/projects/personal/nani/omg.js:13:8) at Module._compile (module.js:653:30) at Object.Module._extensions..js (module.js:664:10) at Module.load (module.js:566:32) at tryModuleLoad (module.js:506:12) at Function.Module._load (module.js:498:3) at Function.Module.runMain (module.js:694:10) at startup (bootstrap_node.js:204:16) at bootstrap_node.js:625:3 Caused by: SyntaxError: Unexpected token i in JSON at position 0 at JSON.parse (<anonymous>) at Object.<anonymous> (/home/sripberger/projects/personal/nani/omg.js:11:7) at Module._compile (module.js:653:30) at Object.Module._extensions..js (module.js:664:10) at Module.load (module.js:566:32) at tryModuleLoad (module.js:506:12) at Function.Module._load (module.js:498:3) at Function.Module.runMain (module.js:694:10) at startup (bootstrap_node.js:204:16) at bootstrap_node.js:625:3 */ ``` `getFullStack` works even if the provided error is not an instance of `NaniError`, so you can safely use it even if you might be dealing with a plain old JS Error. If the error has no `cause` property, it will simply print out the stack as normal. ## Error Info Like `VError`, `NaniError` supports arbitrary data in the form of the `info` object, which can be used to provide further detail about what happened. This will be available as the `info` property on the instance: ```js const err = new NaniError({ shortMessage: 'Omg bad error!', info: { foo: 'bar', baz: 'qux' } }); console.log(err.info.foo); console.log(err.info.baz); /* Running the above code logs the following: bar qux */ ``` Since digging through the whole cause chain for all of its info can be tedious, Nani provides the `collapseInfo` function, which assigns the properties from all info objects in the chain together into a single object: ```js const err = new NaniError({ shortMessage: 'Omg bad error!', info: { foo: 'bar' }, cause: new NaniError({ shortMessage: 'Cause of bad error', info: { baz: 'qux' } }) }); console.log(collapseInfo(err)); /* Running the above code would log the following: { foo: 'bar', baz: 'qux' } */ ``` If the same info property name is encountered more than once in the chain, the value *earlier* in the chain is prioritized. ## Shorthand Constructors Much of the time the `shortMessage` and `cause` options are the only ones you need when creating `NaniError` instances, so the constructor also supports shorthand signatures like so: ```js // shortMessage only. throw new NaniError('Omg bad error!'); // cause only. throw new NaniError(new Error('Cause of the error')); // shortMessage and cause. throw new NaniError('Omg bad error!', new Error('Cause of the error')); // shortMessage, cause, and additional options. throw new NaniError('Omg bad error!', new Error('Cause of the error'), { // Additional options can be included here... hideCauseMessage: true, info: { foo: 'bar' } }); // default everything throw new NaniError(); ``` ## Iterating Through the Cause Chain For convenience, Nani provides a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function) called `iterate`, which makes it easy to step through an error and it's cause chain: ```js const { NaniError, iterate } = require('nani'); const err = new NaniError({ shortMessage: 'foo', cause: new NaniError({ shortMessage: 'bar', cause: new Error('baz') }) }); for (const e of iterate(err)) { console.log(e.message); } /* Running the above code logs the following: foo : bar : baz bar : baz baz */ ``` As you can see, the error itself appears first, followed by each of its causes in sequence. `iterate` works even if the error is just a plan old JS error with no cause, so you can safely use it even when you're not sure the cause chain will be there: ```js const err = new Error('Omg bad error!'); for (const e of iterate(err)) { console.log(e.message); } /* Running the above code logs the following: Omg bad error! */ ``` ### A Note On Circular References Generally, you want to avoid having any references on your errors that point to errors earlier in the cause chain, as doing so can lead to infinite loops and other critical errors, especially if you need to serialize your errors. In the event that you *do* cause such a circular reference, the `iterate` function will ignore it. This is really more to make Nani's functions more robust, but you should probably never do this on purpose: ```js const { NaniError, iterate } = require('nani'); const cause = new NaniError('Cause of error'); const err = new NaniError('Omg bad error!', cause); // Circular reference created here. // Again, *you should never actually do this!* cause.cause = err; for (const e of iterate(err)) { console.log(e.message); } /* Running the above code logs the following: Omg bad error! : Cause of error Cause of error */ ``` ### Iteration Utilities In addition to the `iterate` function itself, Nani includes utility functions for operations you're commonly going to want to do while iterating: - `find`: Returns the first error in the chain matching a predicate function. - `filter`: Returns all errors in the chain that match a predicate function. ```js const { NaniError, find, filter } = require('nani'); const err = new NaniError({ shortMessage: 'foo', cause: new NaniError({ shortMessage: 'bar', info: { isCool: true }, cause: new NaniError({ shortMessage: 'baz', info: { isCool: false }, cause: new NaniError({ shortMessage: 'qux', info: { isCool: true } }) }) }) }); console.log(find((e) => e.info && e.info.isCool)); // { NaniError: bar : baz : qux } console.log(filter((e) => e.info && e.info.isCool)); // [ { NaniError: bar : baz : qux }, { NaniError: qux } ] ``` ## MultiErrors Sometimes you run into situations where you need to collect multiple errors together and present them as one, instead of simply failing at the first. Common use cases for this include validation-- where you may want to display to the user all the problems, instead of just the first one your validation code encountered-- or collections of async operations where some operations may fail independently of the others. Like `VError`, Nani provides a `MultiError` class for handling these situations: ```js const { MultiError } = require('nani'); const err = new MultiError([ new Error('foo'), new Error('bar') ]); // Message only shows the first error, but notes that there are more. console.log(err.message); // First of 2 errors: foo // The first error in the list is treated as the primary cause: console.log(err.cause); // { Error: foo } // Full error list is available as the `errors` property: console.log(err.errors); // [ { Error: foo }, { Error: bar } ] ``` For convenience, you can also provide the error list directly as arguments to the constructor, instead of wrapping them in an array. The following is equivalent to the constructor call above: ```js const err = new MultiError(new Error('foo'), new Error('bar')); ``` ### Creating MultiErrors You can use the `MultiError` constructor directly as shown above, or you can use Nani's `fromArray` utility function. This function is similar to VError's `errorFromList` method, in that it will wrap an array of errors in a `MultiError` if and only if it is necessary to do so: - If the provided array is empty, it returns `null`. - If the provided array has only one element, it returns that element. - If the provided array has more than one element, it returns a `MultiError` with the array as its `errors` property. ```js const { NaniError, fromArray } = require('nani'); // Assume this returns an array with some unknown number of errors... const causes = getSomeErrors(); // This will either have null or a single error as its cause. throw new NaniError('Some bad things maybe happened', fromArray(causes)); ``` ### Iterating MultiErrors Unlike VError, Nani makes it easy to iterate not just through primary causes, but through the entire cause chain of every error in your `MultiError`s, as well. Simply use the `iterate` function: ```js const { NaniError, MultiError, iterate } = require('nani'); const err = new NaniError({ shortMessage: 'outermost error', cause: new MultiError( new NaniError('inner error', new Error('innermost error')), new NaniError('another inner error') ) }); for (const e of iterate(err)) { console.log(e.message); } /* Running the above code will log the following: outermost error : First of 2 errors : inner error : innermost error First of 2 errors : inner error : innermost error inner error : innermost error innermost error another inner error */ ``` As you can see, `iterate` steps through the entire cause chain of each error in a `MultiError` before proceeding to the next. As long as you build your error structures well, you can be certain that `iterate` will touch every Error instance in the entire structure. The behavior demonstrated above holds true for the previously-described iteration utilities-- `find` and `filter`-- in addition to the `iterate` function itself. It is also reflected in `collapseInfo`, which processes each error in the chain in the same order. ### A Note On Duplicate References MultiErrors allow you to potentially duplicate references to the exact same object in your error structures. This should generally be avoided for reasons similar to the circular references described above. In the event that you *do* duplicate an error in the structure, `iterate` will yield it only once, and ignore it from that point forward: ```js const { NaniError, iterate } = require('nani'); const nestedErr = new NaniError('Nested error'); const err = new MultiError(nestedErr, nestedErr); for (const e of iterate(err)) { console.log(e.message); } /* Running the above code logs the following: Nested error */ ``` ## Customizing the Default Message If you have an error message that you find yourself using a lot, you can easily replace the usual generic default error message by simply inheriting from `NaniError` and overriding the static `getDefaultMessage` method: ```js const { NaniError } = require('nani'); class MyError extends NaniError { static getDefaultMessage() { return 'Holy crap, bad stuff happened!'; } } throw new MyError(); /* Running the above code produces something like the following: MyError: Holy crap, bad stuff happened! at Object.<anonymous> (/home/sripberger/projects/personal/nani/omg.js:9:7) at Module._compile (module.js:635:30) at Object.Module._extensions..js (module.js:646:10) at Module.load (module.js:554:32) at tryModuleLoad (module.js:497:12) at Function.Module._load (module.js:489:3) at Function.Module.runMain (module.js:676:10) at startup (bootstrap_node.js:187:16) at bootstrap_node.js:608:3 */ ``` ### Putting Info in the Default Message The `getDefaultMessage` method receives a single argument, which will be a reference to the `options.info` object, if any, or a empty object otherwise. This makes it easy to include some of that info in your error messages: ```js const { NaniError } = require('nani'); class MyError extends NaniError { static getDefaultMessage(info) { return `Holy crap, ${info.what || 'bad stuff'} happened!`; } } throw new MyError({ info: { what: 'terrible things' } }); /* Running the above code produces something like the following: MyError: Holy crap, terrible things happened! at Object.<anonymous> (/home/sripberger/projects/personal/nani/omg.js:9:7) at Module._compile (module.js:635:30) at Object.Module._extensions..js (module.js:646:10) at Module.load (module.js:554:32) at tryModuleLoad (module.js:497:12) at Function.Module._load (module.js:489:3) at Function.Module.runMain (module.js:676:10) at startup (bootstrap_node.js:187:16) at bootstrap_node.js:608:3 */ ``` ## The Dilemma: Identifying Standard JS Errors Though it is generally favored by most JS developers, the practice of [duck typing](https://en.wikipedia.org/wiki/Duck_typing) tends to fall flat when it comes to error handling. `Error` instances are all fairly similar to each other and don't really have any behavior-- i.e. methods-- to use as your 'does it walk' or 'does it quack' tests. To identify what kind of error you're looking at, you need some other mechanism. One way is to look at the properties of the error itself. An `Error` instance has only two standard properties-- `message` and `name`. `message` doesn't work because error messages are meant to be human-readable and are frequently subject to change. Identifying errors based on their message can thus be messy and quite unreliable. `name` potentially works, and is the preferred method of many developers. Every kind of error can have a unique `name` which is machine-readable unlikely to change. Unfortunately, this approach can be rather limiting compared to traditional type systems, as it does not support hierarchies. For example, if you wanted a set of possible password validation errors-- one that indicates that the password is too short, one that indicates that the password doesn't have at least one uppercase character, and another that indicates the password doesn't have at least one number, you might construct and throw them like so: ```js const err = new Error('Too short'); err.name = 'TooShortError'; throw err; ``` ```js const err = new Error('Needs at least one uppercase letter'); err.name = 'NoUppercaseError'; throw err; ``` ```js const err = new Error('Needs at least one number'); errr.name = 'NoNumberError'; throw err; ``` Now you can easily tell these errors apart in consuming code by checking their names: ```js try { validatePassword(password); } catch (err) { if (error.name === 'TooShortError') { // Handle TooShortError. } else if (error.name === 'NoUppercaseError') { // Handle NoUppercaseError } else if (error.name === 'NoNumberError') { // Handle NoNumberError } else { // Rethrow an unknown error. throw err; } } ``` Now, what if you decide you want to specify some more general code, to handle any kind of password validation error without necessarily caring about which specific *kind* of password validation error it is? Without changing the error names, all we really can do is check for each possible name, which as you can imagine gets out of hand rather quickly as you add more kinds of errors: ```js try { validatePassword(password); } catch (err) { if ( error.name === 'TooShortError' || error.name === 'NoUppercaseError' || error.name === 'NoNumberError' ) { // Handle any kind of password validation error. } else { // Rethrow an unknown error. throw err; } } ``` This approach is also a huge pain for anybody consuming your code. You might add a new kind of password validation error, and they'll potentially have to update all of their handling code to check for it. Another possible approach is to start prefixing our error names in some way, and check for the prefix instead of checking the entire name: ```js const err = new Error('Too short'); err.name = 'PasswordTooShortError'; throw err; ``` ```js const err = new Error('Needs at least one uppercase letter'); err.name = 'PasswordNoUppercaseError'; throw err; ``` ```js const err = new Error('Needs at least one number'); errr.name = 'PasswordNoNumberError'; throw err; ``` ```js try { validatePassword(password); } catch (err) { if (err.name.startsWith('Password')) { // Handle any kind of password validation error. } else { // Rethrow an unknown error. throw err; } } ``` This last approach isn't terrible, but figuring out the proper prefix can be difficult and prone to mistakes as there's no standard to follow. Collisions with prefixes and names from elsewhere are likely, and again... if you change the names of any of your errors, all of the handling code has to change. While we've discussed `message` and `name`, there are also plenty of non-standard properties out there that you'll find for accomplishing something like this. Node itself tends to use `code`, which is a machine-readable string that is unlikely to change. Others might have numeric `code` or `errno` properties. Aside from their separation from the standard `name` property that effects the stack trace of the error, these are generally subject to the same limitations that `name` is subject to. There's no standard, and there is no way of easily supporting error type hierarchies. Numeric identifiers in particular can be a pain, because they force you to look them up in documentation instead of just knowing what they are by reading them. Long story short, identifying errors in JS kind of sucks. :\ ### What About `typeof` and `instanceof`? Those who are unfamiliar with the quirks Node development and JS as a language may be tempted to look into JS's standard [typeof](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) and [instanceof](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) operators to accomplish something similar to the static-type-based error handling of languages like Java and C#. The problem you'll run into *immediately* with `typeof` is that it generally tells you next to nothing about non-primitives (which includes Error instances): ```js console.log(typeof 42); // number console.log(typeof 'asdf'); // string console.log(typeof true); // boolean // Ok, so far so good, now let's try some error instances. console.log(typeof new Error('omg')); // object console.log(typeof new TypeError('wow')); // object ``` Right, so that isn't useful. How about `instanceof`? After all, we can easily make our error types into subclasses using ES6 class syntax: ```js class PasswordValidationError extends Error {} class TooShortError extends PasswordValidationError {} class NoUppercaseError extends PasswordValidationError {} class NoNumberError extends PasswordValidationError {} ``` You could also use its approximate equivalent in ES5: ```js function PasswordValidationError(message) { Error.call(this, message) } PasswordValidationError.prototype = Object.create(Error.prototype); function TooShortError(message) { PasswordValidationError.call(this, message) } TooShortError.prototype = Object.create(PasswordValidationError.prototype); function NoUppercaseError(message) { PasswordValidationError.call(this, message) } NoUppercaseError.prototype = Object.create(PasswordValidationError.prototype); function NoNumberError(message) { PasswordValidationError.call(this, message) } NoNumberError.prototype = Object.create(NoNumberError.prototype); ``` With either of these, you can then create and throw your errors like this: ```js throw new TooShortError('Password is too short'); ``` ```js throw new NoUppercaseError('Password must have an uppercase letter'); ``` ```js throw new NoNumberError('Password must have a number'); ``` Then, ideally, you can handle your errors like this: ```js try { validatePassword(password); } catch (err) { if (err instanceof TooShortError) { // Handle the too short error. } else if (err instanceof PasswordValidationError) { // Handle any other kind of password validation error. } else { // Rethrow an unknown error. throw err; } } ``` If you try this approach, the results might seem promising at first, but there is a relatively subtle limitation of `instanceof` that can and will cause you some major headaches. While it may seem similar at a glance `instanceof` is *not* an equivalent to true static type checking, which is not possible in vanilla JS. `instanceof` merely approximates, by searching the first operand's prototype chain to see if it ever contains the exact same object as the `prototype` property of its second operand. This will never give you a false positive, but it *can* give you false negatives if you're ever in a situation where an object was created with a *copy* of the second operand. Yes, the prototype objects might be similar, but they are not literally the exact same object, which causes the check to fail. The classic example of this from web browsers is when dealing with instances passed across frames and/or iframes. In Node, this same issue might happen when you have multiple versions of the same constructor in your dependency tree, a not-uncommon situation that can arise when: - Conflicting dependency semver expressions require two different versions of the same module. - Some installed modules can share the same version, but not yet been deduped. - `npm link` is used for anything whatsoever. Additionally, when you're transferring errors between various services and instances of services-- as is common in Node architectures-- you may need to serialize them and then rebuild them elsewhere. In order for `instanceof` to keep working, you would need to re-instantiate every error, and every error in its cause chain, using the same constructors. This can be complicated and fairly prone to mistakes. In any of the above scenarios, the above `catch` block could rethrow a `PasswordValidationError`. If that error is not caught somewhere further up the call stack, it could of course crash your entire app. Obviously this is a huge problem, so long story short: `instanceof`, while it has its uses, is not really robust enough for this purpose. ### A Solution: Full Names To solve this dilemma-- allowing you to do something like traditional error type-checking without relying on `instanceof`-- Nani uses a property called `fullName`. It's similar to the prefixing solution discussed above, except that full names follow a simple, predictable format and are easy to generate based on syntax you may already be using. The basic idea is this: An error class's name is simply the name of its constructor function. It's fullName, however, is a dot-separated list of all of the constructor names in its inheritance chain. Both of these properties are available on all instances of error constructors, as well. The `NaniError` class implements these properties, so as long as you include it at the base of your hierarchies, generating fullNames requires no effort on your part. For example, to make the password validation hierarchy discussed above, it's just this simple: ```js const { NaniError } = require('nani'); class PasswordValidationError extends NaniError {} class TooShortError extends PasswordValidationError {} class NoUppercaseError extends PasswordValidationError {} class NoNumberError extends PasswordValidationError {} console.log(PasswordValidationError.fullName); // Error.NaniError.PasswordValidationError console.log(TooShortError.fullName); // Error.NaniError.PasswordValidationError.TooShortError console.log(NoUppercaseError.fullName); // Error.NaniError.PasswordValidationError.NoUppercaseError console.log(NoNumberError.fullName); // Error.NaniError.PasswordValidationError.NoNumberError // Name and fullName are automatically available on instances as well. const err = new NoUppercaseError(); console.log(err.name); // NoUppercaseError console.log(err.fullName) // Error.NaniError.PasswordValidationError.NoUppercaseError ``` Of course, not every error instance or error class you're going to be dealing with will inherit from `NaniError`, so Nani provides the function `getFullName`, which, for convenience, attempts to support these through a simple mechanism: 1. If a fullName property exists, use that. 2. If if the name property is 'Error', the fullName is also assumed to be 'Error'. 3. If the name property *ends with* 'Error', the fullName is assumed to be the name property appended to 'Error', separated by a dot. 4. In all other cases, the fullName is assumed to be `null`, indicating that it can't be identified. For example: ```js const { NaniError, getFullName } = require('nani'); class PasswordValidationError extends NaniError {} class TooShortError extends PasswordValidationError {} class MyCustomError extends Error {} console.log(getFullName(PasswordValidationError)); // Error.NaniError.PasswordValidationError console.log(getFullName(TooShortError)); // Error.NaniError.PasswordValidationError.TooShortError console.log(getFullName(Error)); // Error console.log(getFullName(TypeError)); // Error.TypeError console.log(getFullName(MyCustomError)); // Error.MyCustomError ``` Since all standard Error subclass names end in 'Error'-- as do most custom ones-- you can reliably use `getFullName` much of the time. Of course, if you already have your own error name hierarchies that aren't based on the `fullName` property, these will not be supported. ### Using Full Names to Identify Errors To make easy use of full names for identification, Nani provides the `is` function. `is` will get the full name of both of its arguments, and will return true if and only if both full names can be identified and the first argument's full name *starts with* the second argument's full name. This makes checks against your hierarchies read quite naturally. Going back to our password validation example, you can declare your error classes like so: ```js class PasswordValidationError extends NaniError {} class TooShortError extends PasswordValidationError {} class NoUppercaseError extends PasswordValidationError {} class NoNumberError extends PasswordValidationError {} ``` Throw your errors like so: ```js throw new TooShortError('Password is too short'); ``` ```js throw new NoUppercaseError('Password must have an uppercase letter'); ``` ```js throw new NoNumberError('Password must have a number'); ``` And handle your errors like so: ```js const { is } = require('nani'); try { validatePassword(password); } catch (err) { if (is(err, TooShortError)) { // Handle the too short error. } else if (is(err, PasswordValidationError)) { // Handle any other kind of password validation error. } else { // Rethrow an unknown error. throw err; } } ``` This mechanism therefore gives you the advantages of `instanceof`, without actually relying on `instanceof`. ### Shorthand Forms for Iteration Utilities Since the `is` function is likely to be used quite a bit with them, the `find` and `filter` iteration utilities support a shorthand form. If the predicate is an Error constructor-- i.e. it has a fullName that starts with `Error`-- the predicate will return true for any iterated item that `is` that constructor. For example: ```js const { find, filter, is } = require('nani'); const err = new NaniError(); let result; // These two statements are effectively equivalent: result = find(err, (e) => is(e, NaniError)); result = find(err, NaniError); // As are these two: result = filter(err, (e) => is(e, NaniError)); result = filter(err, NaniError); ``` ### Namespacing Your Errors Considering the way full names work, it *is* possible to have collisions, especially if you use fairly general error names that others are likely to use in their own projects, like `ValidationError` or `InternalError`. To deal with this, I recommend that you make a single base error for all of your hierarchies, preferably named based on your organization, or the name of your project. If you're writing a validation library called `foobar`, for example, instead of just doing this: ```js const { NaniError } = require('NaniError') class ValidationError extends NaniError {} ``` Try doing something like this: ```js const { NaniError } = require('NaniError') class FoobarError extends NaniError {} class ValidationError extends FoobarError {} ``` ## Full Iteration As discussed before, the `iterate` generator function simply yields all of the errors in an error structure in sequence, skipping any duplicates. This is usually what you want, but sometimes you need more information about the structure itself-- if you're writing a serializer, for example. For this purpose, nani provides `iterateFull`. It is similar to `iterate`, except that it does not skip duplicate references, and yields objects with some information about each error's context within the structure. It *does* check for circular references that would create an infinite loop, however, and ignores those just to be safe. Error info is yielded in the form of objects with three properties: - `err` - The Error instance. - `parent` - The parent of `err`, if any. `null` otherwise. - `inArray` - `true` if `err` is in an `errors` array on its parent, `false` otherwise. ```js const { NaniError, MultiError, iterateFull } = require('nani'); const outerErr = new NaniError({ shortMessage: 'outermost error', cause: new MultiError( new NaniError('inner error', new Error('innermost error')), new NaniError('another inner error') ) }); for (const { err, parent, inArray } of iterateFull(outerErr)) { console.log(`Message: ${err.message}`); console.log(`Parent Message: ${parent ? parent.message : null}`); console.log(`In Array: ${inArray}\n`); } /* Running the above code will log the following: Message: outermost error : First of 2 errors : inner error : innermost error Parent Message: null In Array: false Message: First of 2 errors : inner error : innermost error Parent Message: outermost error : First of 2 errors : inner error : innermost error In Array: false Message: inner error : innermost error Parent Message: First of 2 errors : inner error : innermost error In Array: true Message: innermost error Parent Message: inner error : innermost error In Array: false Message: another inner error Parent Message: First of 2 errors : inner error : innermost error In Array: true */ ```