DEV Community

loading...

Wrapping Errors

alexsasharegan profile image Alex Regan Originally published at blog.parametricstudios.com on ・6 min read

When performing I/O in Node.js servers, there is a lot of error handling to deal
with. I always look to add as much context to errors as I can because it greatly
reduces the time it takes me to find, understand, and fix bugs.

We won't go into an error propagation strategy here (that will be part of some
of the upcoming material)
, but I wanted to share a quick idea for how to
decorate errors with context as they propagate through the stack.

Native Error

JavaScript comes with a few built-in error classes. They include:

  • Error: a base class for exceptions.
  • EvalError: an error regarding the global eval() function.
  • InternalError: an error that occurred internally in the JavaScript engine.
  • RangeError: an error when a value is not in the set or range of allowed values.
  • ReferenceError: an error when a non-existent variable is referenced.
  • SyntaxError: an error when trying to interpret syntactically invalid code.
  • TypeError: an error when a value is not of the expected type.
  • URIError: an error when a global URI handling function was used in a wrong way.

While we may sometimes find usages for specific built-ins like the RangeError,
most user-defined exceptions make use of the base Error class. Its properties
include a name, message, and stack. In Node.js, error instances
additionally contain a code which identifies various error types including
underlying system errors.

Creating our own error (and throwing it) looks something like this:

function parseIntStrict(numLike) {
  let n = parseInt(numLike, 10);
  if (!Number.isInteger(n)) {
    throw new Error(`unable to parse input as integer: ${numLike}`);
  }
  return n;
}

Here we create and throw an error when parseInt returns NaN values. The
message in the error is about as specific as we can make it without more
context. This function might be called to convert user input for updating a
product quantity in a shopping cart, parsing a field from a csv file, etc.--we
can't know how it will be used.

In Context

Let's say we are writing an api that takes a csv file and parses inventory
quantities from its row fields. We're going to use our parseIntStrict function
when we parse a product's inventory count in the file. We might think of our api
call stack like this:

  • [4] Parse quantity field
  • [3] Parse csv row
  • [2] Read file by lines
  • [1] Map request to csv file on disk
  • [0] Receive network request

There are a number of steps here before we use the parseIntStrict function.
Ideally, if a call to our function fails, we would like to know our request id,
the name of the csv file on disk, the row index, and the field that caused the
failure. If we have all that information, we can go straight to solving any
broken functionality instead of exhausting valuable time diagnosing the source
of the problem.

Wrapping Errors

In order to achieve our goal of adding context to an error, we need a way to
decorate errors and then bubble them back up the stack. One way we could do this
is by adding a message representing the current context. We don't want our
message to corrupt the original message in any way. This discourages us from
attempting to manipulate the original error's message, but since we aren't
always guaranteed error producers use the Error class to generate exceptions,
this isn't an option in the first place.

Let's extend the Error class so we can add a reference to the untouched,
original error value. This will let us add our contextual message and keep a
reference to the original error value regardless of it being an Error instance
or not.

class WrappedError extends Error {
  constructor(message /* context */, previous /* original error value */) {
    // First we have to call the Error constructor with its expected arguments.
    super(message);
    // We update the error's name to distinguish it from the base Error.
    this.name = this.constructor.name;
    // We add our reference to the original error value (if provided).
    this.previous = previous;
  }
}

We aren't changing much from the base Error class. We'll get the default
Error behaviors for free since we inherit (generating stack traces,
stringification, etc.)
, but now our error type can reference a previous error.
If we use a WrappedError to generate our first exception, the previous error
will be undefined. If we create an instance passing in an Error or a literal
value, previous will maintain a reference to it. Most interestingly, if we
create an instance passing in another WrappedError instance, we connect a
chain of errors together.

When we start using our WrappedError throughout our application code, we
essentially transform our error values to linked lists. The list head would be
our current error's position in the stack, or its context. We could traverse
the list by checking for the existence of a previous error, and if it exists,
checking to see if it's an instance of WrappedError, which guarantees us a
previous field we can inspect to continue the traversal.

Let's look at how we might perform a traversal to get at the root error
value--be it a WrappedError, Error, or otherwise. We'll use a computed
property--an
object getter method--to
allow us to compute and access the root value like a property.

class WrappedError extends Error {
  // ...

  get root() {
    // Base case, no child === this instance is root
    if (this.previous == null) {
      return this;
    }
    // When the child is another node, compute recursively
    if (this.previous instanceof WrappedError) {
      return this.previous.root;
    }
    // This instance wraps the original error
    return this.previous;
  }
}

let error1 = new Error("the first error");
let error2 = new WrappedError("the second error wraps #1", error1);
let error3 = new WrappedError("the third error wraps #2", error2);

console.assert(
  error2.root === error1,
  "the root of error2 should be strictly equal to error1"
);
console.assert(
  error3.root === error1,
  "the root of error3 should be strictly equal to error1"
);
// Passes if no error appears in the console

You can test this example in
this repl here, but suffice
it to say that the strict equality checks are all true. There's a lot we can do
here, but simply logging with console.error produces this output in Node.js
(from the same repl):

{ WrappedError: the third error wraps #2
    at evalmachine.<anonymous>:27:14
    at Script.runInContext (vm.js:74:29)
    at Object.runInContext (vm.js:182:6)
    at evaluate (/run_dir/repl.js:133:14)
    at ReadStream.<anonymous> (/run_dir/repl.js:116:5)
    at ReadStream.emit (events.js:180:13)
    at addChunk (_stream_readable.js:274:12)
    at readableAddChunk (_stream_readable.js:261:11)
    at ReadStream.Readable.push (_stream_readable.js:218:10)
    at fs.read (fs.js:2124:12)
  name: 'WrappedError',
  previous:
   { WrappedError: the second error wraps #1
    at evalmachine.<anonymous>:26:14
    at Script.runInContext (vm.js:74:29)
    at Object.runInContext (vm.js:182:6)
    at evaluate (/run_dir/repl.js:133:14)
    at ReadStream.<anonymous> (/run_dir/repl.js:116:5)
    at ReadStream.emit (events.js:180:13)
    at addChunk (_stream_readable.js:274:12)
    at readableAddChunk (_stream_readable.js:261:11)
    at ReadStream.Readable.push (_stream_readable.js:218:10)
    at fs.read (fs.js:2124:12)
     name: 'WrappedError',
     previous: Error: the first error
    at evalmachine.<anonymous>:25:14
    at Script.runInContext (vm.js:74:29)
    at Object.runInContext (vm.js:182:6)
    at evaluate (/run_dir/repl.js:133:14)
    at ReadStream.<anonymous> (/run_dir/repl.js:116:5)
    at ReadStream.emit (events.js:180:13)
    at addChunk (_stream_readable.js:274:12)
    at readableAddChunk (_stream_readable.js:261:11)
    at ReadStream.Readable.push (_stream_readable.js:218:10)
    at fs.read (fs.js:2124:12) } }

Not bad, right?

Open For Extension

The contextual references we're able to create with this provide a lot of
opportunities for extension. Depending on the needs of your application, you
might consider:

I've implemented the spans property in the
repl link if you're
curious. If you think of new applications or features for this idea, you can
find me on Twitter at @alexsasharegan.

Discussion (1)

pic
Editor guide
Collapse
alexsasharegan profile image
Alex Regan Author

A typescript example I've starting using in a new project: gist.github.com/alexsasharegan/456...