Error wrappers

Almost all applications eventually need to create custom error classes. Often, these custom errors are "wrappers" for existing ones, that add application-specific context. For example, a node database migration tool might try-catch each migration, and re-raise a MigrationError which tells the user migration 2020-12-29-create-user-table.sql failed, whereas the underlying error might say something like Syntax error near ';', which is what comes from the database client.

The idea here is a WrappedError class which extends Error to standardise the Right Way to do this. It would need to be able to provide, at minimum, a custom message and a cause property, which is the error (or other throwable) object which it's wrapping. The best existing prior art I've come across is verror: GitHub - TritonDataCenter/node-verror: Rich JavaScript errors . The design is well thought through, simple and useful (although the naming is a bit cryptic).

To borrow some examples from verror:

var err1 = new Error('No such file or directory');
var err2 = new VError(err1, 'failed to stat "%s"', '/junk');
var err3 = new VError(err2, 'request failed');
console.error(err3.message)

This prints:

request failed: failed to stat "/junk": No such file or directory

The idea is that each layer in the stack annotates the error with a description of what it was doing. The end result is a message that explains what happened at each level.

The two main goals of verror are stated as:

  • Make it easy to construct clear, complete error messages intended for people. Clear error messages greatly improve both user experience and debuggability, so we wanted to make it easy to build them. That's why the constructor takes printf-style arguments.
  • Make it easy to construct objects with programmatically-accessible metadata (which we call informational properties). Instead of just saying "connection refused while connecting to 192.168.1.2:80", you can add properties like "ip": "192.168.1.2" and "tcpPort": 80. This can be used for feeding into monitoring systems, analyzing large numbers of Errors (as from a log file), or localizing error messages.

There are some problems with this functionality existing in a library:

  1. its maintenance status is unclear. Verror hasn't seen any GitHub changes for over a year, and the npm package hasn't been published for four years.
  2. it being an individual (and possibly unmaintained) library means that tooling is unlikely to take account of it. It doesn't make sense for error reporting systems like Sentry to support verror out of the box. If it was part of the language spec it absolutely would. There could be first-class UI for an on-call engineer to dive into production errors, seeing the top-level one first and click through the chain of causes.
  3. some libraries do use verror, but most don't, using instead a custom error-wrapping system which doesn't always maintain the original error, or capture it in a different way. Meaning investigating root-cause issues can become difficult or impossible.
  4. Standard reason of it being nice to be able to avoid a dependency for very important, very broadly useful functionality.

Being part of the language spec would cause a positive feedback loop of goodness. Tooling could start adding UI/DX around wrapped errors, which would encourage library and application developers to wrap their errors in a standardised way, further improving tooling and the ecosystem in general.

Just having an accessible chain of causes would be valuable, but I think it'd be worth incorporating the rest of the verror features too - a standard way of defining additional properties on an error, and printf style formatting for error messages, which would make them easier to group and anonymise. All of this being in the language spec would allow debugging tools to greatly improve, and would become available in a standard way to all js environments (node, browsers, deno, plv8, etc. etc.). It'd be a good opportunity to add those features, but if printf for example was controversial it could be changed or removed.

1 Like

You might be interested in the Stage 2 Error cause proposal

3 Likes

@aclaymore thanks - I searched for it but I guess I used bad keywords. I'll take a deeper look in that proposal and raise anything I feel could/should be added as issues there.

1 Like

Almost all applications eventually need to create custom error classes

I disagree. All errors are typically treated the same in frontend programming -- e.g. the ui will crash, and log/notify user of error message and/or code for debugging purpose.

Creating custom error classes is needless tech debt. u'll be hard-pressed to find an end-user who cares about the differentiation.

That is quite false. Frontend programming is no different than any other kind here - some errors are allowed to crash the program, and some errors are intercepted and handled.

@kaizhu256 I agree to some extent, but:

  1. JS is not just for front end programming.
  2. Even if it were, errors aren't always surfaced to the end user, so whether end users care about the differences between errors isn't really relevant.
  3. Even in cases where end users do see errors, it's still possible to handle subclasses the same as standard Error instances.
  4. Some errors are caught and conditionally rethrown.
  5. Even if the application treats all (uncaught) errors the same, it's still useful to capture relevant information in a standardised way in error loggers like Sentry.
  6. This idea would actually reduce the need to subclass Error. A WrappedError would be sufficient for a lot more use cases than the current error. Either way, the stage 2 proposal linked above doesn't subclass Error at all, it just adds a second constructor parameter.
1 Like

as full-stack programmer, the only errors worth raising in userland are unrecoverable ones -- and the only thing that matters in those errors are the error-code, error-message, and nice-to-have stack-trace.

if it's a recoverable error, e.g. timeout, ENOENT, EEXISTS, etc. you should not be passing it off to a clueless end-user. take charge and handle the recovery/retry/mkdirp/db-reconnect yourself as a full-stack programmer, w/o ever having to raise a custom error.

A custom error is how one part of your program communicates to another that an error is recoverable. Whether that's by using a parseable string message, additional properties like an error code, or a specific subclass, it's still a custom error (including "ENOENT").

1 Like