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.
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 mytoString()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