DEV Community

Evan K
Evan K

Posted on

Stringifying Errors: A Uniquely Javascript Problem

Exception handling, like most things in the Javascript world, is...complicated.

What is or isn't supported may vary widely between client (browser) and server (NodeJS), and further even from one browser to another.

If you're not too familiar already, this is a great guide to all things Javascript error handling.

The short version goes like this:

  • Exception handling typically involves throwing and catching exceptions, either synchronously (in a try...catch block) or asynchronously (with Promise.prototype.catch).
  • Javascript provides a standard built-in Error class for you to use, either directly or with extending sub-classes.
  • The standard Error class comes with useful but not-always-supported (and so not-always-used) features like the stack and cause properties.
  • The throw statement allows you to throw a user-defined exception, which is notably not required to be an Error object.

The Error class

Error objects are great, but the biggest headache around handling them typically involves stringifying or serializing them, as is evident from this NodeJS console example:

> new Error('a problem occurred').toString()
'Error: a problem occurred'
> JSON.stringify( new Error('a problem occurred') )
'{}'
Enter fullscreen mode Exit fullscreen mode

The error object in question — an instance of the Error class — supports conversion to a string via its toString method. This produces (essentially) a concatenation of the name and message, omitting the stack and any other instance properties. Sometimes this is good, as you just want to know what broke but not necessarily where.

Serialization to JSON is decidedly unhelpful, returning an empty object.

Logging to different destinations

For debugging and most logging purposes, the built-in console.log method prints a friendly and verbose summation of any error directly to stdout (in NodeJS), or to the web console (in browser):
side by side of node.js and browser console.logs of an error object

And if instead you need it printed to stderr, look no further than the console.error method.

Well, what if you need to log an error somewhere else? To a file, a database table, a third party logging utility, etc.

You might wonder if there's a way to capture this same output from the console to a string variable, and in NodeJS you technically could. It is not simple, however, and one approach requires another console instance and some trickery with streams.

Streams give me anxiety, so I went another way.

Enter loggable-error

A little over two kilobytes of javascript, with zero runtime package dependencies and a single default export.

It produces a string approximate to what console.log would print — an Error object expanded to constructor name, message and stack, with any other instance properties in an object notation:

import stringify from 'loggable-error';

class CustomError extends Error {
    constructor(message, options) {
        super(message, options);
        this.extra = options?.extra;
    }
}

try {
    throw new Error(
        'testing one two three',
        {
            cause: new CustomError(
                'the actual root cause',
                {
                    extra: {
                        message: 'some extra debugging info',
                        values: [ 3.14, 42, null ]
                    }
                }
            )
        }
    );
} catch(e) {
    process.stdout.write(
        stringify(e)
    );
}
/* =>
Error: testing one two three
    at file:///home/jdoe/test.js:11:11
    at ModuleJob.run (node:internal/modules/esm/module_job:222:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:123:5) {
  [cause]: CustomError: the actual root cause
      at file:///home/jdoe/test.js:14:20
      at ModuleJob.run (node:internal/modules/esm/module_job:222:25)
      at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)
      at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:123:5) {
    [extra]: {"message":"some extra debugging info","values":[3.14,42,null]}
  }
}
*/
Enter fullscreen mode Exit fullscreen mode

As shown above, it also handles error objects nested as properties with recursive calls to itself, indenting them at the appropriate level.

You've got options

Because different situations call for different formatting, the exported function accepts a second options object, allowing you to toggle display of stack traces and control the initial indentation:

// shown here with the default options
stringify(e, { depth: 0, stack: true });
Enter fullscreen mode Exit fullscreen mode

That's all, folks!

The goal here was to keep it simple, but if you have issues or feature requests, I'd love to hear them.

Top comments (0)