Node.js und meine Base-Error-Klasse
Im Laufe der Arbeit mit Node.js bin ich immer wieder an den Punkt gestoßen, dass ich enorme Probleme bei der Fehlerbehandlung hatte. Insbesondere der Aspekt, dass ein Fehler nicht zwangsläufig ein Error-Objekt ist, sondern auch eine Zeichenkette sein kann, führte zu Schwierigkeiten. Ich zeige dir, wie ich dieser Herausforderung begegne.
Mein erster Schritt, um der Lage Herr zu werden, ist die Einführung eines BaseError. In meinem Code werfe ich stets Fehler, die von dieser Klasse abgeleitet werden. Mithilfe dieses Ansatzes kann ich mich bei der Fehlerbehandlung darauf beschränken, Fehler Dritter in diese Klasse zu übertragen und ansonsten die korrekte Behandlung von BaseError zu implementieren.
Der Base-Error
Meinen Base-Error leite ich von der JavaScript-Klasse Error ab. Es gibt keinen Grund von diesem Sprachkonstrukt abzuweichen. Zusätzlich bette ich noch zwei Methoden ein, die mir während der täglichen Arbeit helfen:
toJSON()als Hilfsmethode zur Übersetzung eines Fehlers in ein JSON-Objekt, z. B. für das Logging (sofern im JSON-Format erfolgt).toString()als Hilfsmethode zur Serialisierung als Text. Diese verlässt sich intern auf die JSON-Repräsentation. Hierbei ist Vorsicht geboten: Zyklische Referenzen führen bei diesen Aufrufen zu einer Fehlermeldung. Das fange ich in meinertoString()-Funktion ab.
Eine wichtige Zeile möchte ich hervorheben: Error.captureStackTrace(this, this.constructor). Damit wird die Error-Property stack mit wichtigen Details zum StackTrace des Fehlers befüllt.
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() Hilfsmethode
Um den zyklischen Referenzen entgegenzuwirken, nutze ich einen plumpen und effektiven Ansatz. Ich bediene mich des NPM-Pakets flatted. Das ist ein schlanker JSON-Parser, der mit zyklischem JSON umgehen kann. Das Ergebnis ist nicht lesbar, doch besser als die Alternative: entweder nichts ausgegeben zu bekommen oder einen Fehler zu erhalten.
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)
}
}
Entweder JSON.stringify() schafft es oder ich nutze die Methode von flatted
toJSON() für Error-Objekte
Bekanntlich liefert JSON.stringify(error) nur ein leeres Objekt zurück. Der Hintergrund ist, dass das Error-Objekt keine enumerierbaren Properties hat. Um dies an zentraler Stelle zu umgehen, empfehle ich, folgenden kleinen Codeschnipsel einzubauen:
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,
})
}
Eine umfangreiche Diskussion zu dem Thema findet ihr auf StackOverflow