The Big Picture

How it all fits together

The explicit-exceptions package provides stability to your project by allowing you to explicitly specify which exceptions a function call may provide, each step of the way.

In the README, we presented the following example:

const { Exception, wrap, unwrap } = require('explicit-exceptions')

const data = new Map()

const getEntry = wrap(id => {
  if (!data.has(id)) {
    // Throws our custom Exception instance, with the intention that
    // our caller would be able to catch and handle this.
    throw new Exception('NotFound')
  }
  return data.get(id)
})

const getEntryOrDefault = (id, defaultValue=null) => {
  try {
    // This line showcases the most important feature of this package.
    // unwrap() self-documents the fact that getEntity() can provide the "NotFound" exception.
    // Runtime checks will ensure no other exceptions leak through that aren't explicitly named here.
    return unwrap(getEntry(id), ['NotFound'])
  } catch (ex) {
    if (!(ex instanceof Exception)) throw ex // Not an exception, don't handle it
    console.assert(ex.code === 'NotFound')
    return defaultValue
  }
}

Let's pick this apart and learn what each piece is doing.

throw new Exception('NotFound')

Exceptions are designed to be easy to distinguish from one another. In this case, this instance can be distinguished from other exceptions by checking if its code property equals "NotFound".

Only throw exceptions if you want the caller to be able to recover from it - if you've detected a programmer error (like you found yourself in an invalid state), then throw an instance of Error() instead and let your program "crash early".

Any function that can throw instances of Exception should be decorated by wrap().

const getEntry = wrap(id => {
  ...
})

wrap() will take a function and return a new one that captures any thrown exceptions or returned values, and wraps them into a Maybe instance.

Pro tip: If you want your wrapped function to have a name in error stack traces, you'll have to explicitly give it one like this:

const getEntry = wrap(function getEntry(id) {
  ...
})

Maybe instances can only be retrieved from wrapped functions. They're designed to be a black box that holds the result of a wrapped function call. The only thing you can do with the Maybe instance is unwrap it (with unwrap()). In fact, the library will attempt to detect and warn you if you've forgotten to call unwrap() on a Maybe instance.

return unwrap(getEntry(id), ['NotFound'])

Here, getEntry() is a wrapped function that will return a Maybe instance. This Maybe instance holds details about how getEntry()'s inner function behaved during execution. unwrap() will cause one of the following three actions to be performed:

  • If getEntry() inner function did not throw an exception during its execution, then unwrap() will simply return whatever getEntry() inner function returned.

  • If getEntry()'s inner function threw a NotFound exception, then unwrap will cause it to be rethrown, because we've declared that we expected it in the second parameter to unwrap.

  • If getEntry() were to throw a different NotAvailable exception that was not found in our list of expected exceptions, then unwrap will instead throw an instance of Error() explaining that a programmer error has occured. This is called "escalating" the exception.

Note that it's ok, and even encouraged to have unwrap's expected-exceptions parameter not be an exhaustive list of exceptions that the wrapped function can provide. If you have no way of handling the exception, then omit it from the expected-exception list, and let it escalate.

const getEntryOrDefault = (id, defaultValue=null) => {
  try {
    return unwrap(getEntry(id), ['NotFound'])
  } catch (ex) {
    // ...
  }
}

Notice that getEntryOrDefault isn't getting wrapped with wrap(). This is because the only exception being thrown inside (NotFound) is also getting handled before reaching its caller.

interoperability with other exception systems

You may wish to have explicit-exceptions be an internal detail to your package, and keep your package users unaware of its use. This dedicated page explains how to achieve this.

Next Steps

Check out the API Reference to learn more about the finer details of each function. Or, go ahead and give it an install with npm i explicit-exceptions. You may also be interested in the light version, which provides a simplified variation of this project, intended for those who would prefer maintaining around 100 lines of code themself instead of adding a new dependency to their project.

Last updated