Node.js and my Base-Error class

During my work with Node.js, I repeatedly encountered significant problems with error handling. In particular, the fact that an error isn't necessarily an Error object, but can also be a string, caused difficulties. I'll show you how I tackle this challenge.

3 minutes

My first step to getting a handle on the situation is to introduce a BaseError. In my code, I always throw errors that are derived from this class. Using this approach, I can limit my error handling to transferring third-party errors to this class and otherwise implementing the correct handling of BaseError.

The BaseError

I derive my BaseError from the JavaScript class Error. There's no reason to deviate from this language construct. Additionally, I embed two methods that help me in my daily work:

  • toJSON() as a helper method for translating an error into a JSON object, e.g., for logging (if done in JSON format).
  • toString() as a helper method for serializing to text. This relies internally on the JSON representation. Caution: Cyclic references will cause an error when calling this method. I catch this in my toString() function.

I want to highlight one crucial line: Error.captureStackTrace(this, this.constructor). This populates the stack error property with essential details from the error's stack trace.

import { toString } from '@/utils/StringUtils'  

export interface BaseErrorOptions {  
  detail?: string  
  cause?: unknown  
  context?: unknown  
  statusCode?: number  
}  

class BaseError extends Error {  
  public readonly detail: string  
  public readonly cause?: unknown  
  public readonly context?: unknown  
  public readonly statusCode: number  

  constructor(options: BaseErrorOptions) {  
    const { detail = 'Unknown error detail', cause, context } = options  
    super(detail)  
    Error.captureStackTrace(this, this.constructor)  
    this.detail = detail  
    this.cause = cause  
    this.context = context  
    this.statusCode = options.statusCode || 500  
  }  

  public toJSON() {  
    return {  
      detail: this.detail,  
      cause: this.cause,  
      context: this.context,  
      statusCode: this.statusCode,  
      stack: this.stack,  
    }  
  }  

  public toString() {  
    return toString(this.toJSON())  
  }  
}  

export default BaseError

toString() Helper Method

To counteract the cyclic references, I use a clumsy but practical approach. I utilize the NPM package flatted. This is a lightweight JSON parser that can handle cyclic JSON. The result is unreadable, but it's better than the alternatives: either no output or an error.

import { stringify } from 'flatted'

export const toString = (value: unknown): string => {  
  if (value === undefined) {  
    return 'undefined'  
  }  

  if (value === null) {  
    return 'null'  
  }  

  if (typeof value === 'string') {  
    return value  
  }  

  try {  
    return JSON.stringify(value)  
  } catch {  
    return stringify(value)  
  }  
}

Either JSON.stringify() can handle it, or I'll use the flatted method.

toJSON() for Error Objects

As you know, JSON.stringify(error) only returns an empty object. This is because the Error object doesn't have enumerable properties. To circumvent this at a central point, I recommend including the following small code snippet:

if (!('toJSON' in Error.prototype)) {  
  Object.defineProperty(Error.prototype, 'toJSON', {
    value: function () {
      const alt: Record<string, unknown> = {
      }

      Object.getOwnPropertyNames(this).forEach((key: string) => {
        alt[key] = this[key]
      }, this)

      return alt
    },
    configurable: true,  
    writable: true,  
  })  
}

You can find an extensive discussion on this topic at StackOverflow

How do you deal with errors in Node.js?

Let's talk
call to action background image

Subscribe to my newsletter

Receive once a month news from the areas of software development and communication peppered with book and link recommendations.