Domain Errors sauber strukturieren statt new Error("...") ueberall
Die eingebaute Error Klasse aus JavaScript reicht fuer eine wachsende Codebasis selten aus: sie traegt keine Error Codes, keine strukturierten Metadaten und keine saubere Serialisierung. Eine durchdachte Error Klassen Hierarchie loest diese Probleme mit einer gemeinsamen Basisklasse, konsistenten instanceof Pruefungen und Cause Chaining fuer verkettete Fehlerursachen.
Inhaltsverzeichnis
- 1. Warum die eingebaute Error Klasse an ihre Grenzen stoesst
- 2. Eine Basisklasse fuer Domain Errors entwerfen
- 3. Error Codes und Metadata typsicher anhaengen
- 4. instanceof Pruefungen und die Prototype Chain richtig behandeln
- 5. Error Hierarchien fuer unterschiedliche Fehlerkategorien
- 6. Error Serialisierung fuer Logging und API Responses
- 7. Error Klassen mit Cause Chaining verketten
- 8. Typische Fehler beim Bauen von Error Hierarchien
- 9. Ansaetze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum die eingebaute Error Klasse an ihre Grenzen stoesst
Die native Error Klasse in JavaScript ist bewusst minimal gehalten: sie traegt eine message, einen name und, je nach Umgebung, eine stack Eigenschaft. Fuer einfache Skripte reicht das aus, aber sobald eine Anwendung waechst, fehlen genau die Informationen, die eine strukturierte Fehlerbehandlung braucht: ein maschinenlesbarer Error Code, zusaetzliche Kontextdaten wie eine betroffene Entity ID, und eine klare Zuordnung, aus welcher Schicht der Anwendung der Fehler stammt. Eine eigene Error Klassen Hierarchie schliesst genau diese Luecke.
Ohne eine solche Error Klassen Hierarchie greifen viele Teams zu Zeichenketten Vergleichen wie error.message.includes("not found"), um Fehlerarten zu unterscheiden. Dieser Ansatz ist fragil, weil sich Nachrichtentexte bei einer Uebersetzung oder einer kleinen Formulierungsaenderung stillschweigend aendern koennen, was den Vergleich unbemerkt bricht. Eine dedizierte Klasse pro Fehlerart, erkennbar per instanceof, ist robust gegenueber solchen Textaenderungen, weil die Identitaet ueber den Typ und nicht ueber den Nachrichtentext laeuft.
Der dritte Grund fuer eine eigene Error Klassen Hierarchie ist Konsistenz ueber ein ganzes Team hinweg. Ohne gemeinsame Basisklasse entwickelt jeder Entwickler eigene Ad hoc Konventionen fuer Fehlerobjekte, was zu einer wild wachsenden Vielfalt an Formen fuehrt, die weder einheitlich geloggt noch einheitlich an den Client zurueckgegeben werden koennen. Eine gemeinsame Basisklasse mit klaren Konventionen fuer Error Codes, Metadata und Serialisierung schafft hier Ordnung.
2. Eine Basisklasse fuer Domain Errors entwerfen
Der erste Baustein jeder Error Klassen Hierarchie ist eine gemeinsame Basisklasse, oft DomainError oder AppError genannt, von der alle spezifischeren Fehlerklassen erben. Diese Basisklasse erweitert die native Error Klasse und ergaenzt sie um Felder, die in der gesamten Anwendung konsistent verfuegbar sein sollen: einen code fuer maschinenlesbare Identifikation, ein isOperational Flag, das zwischen erwarteten Fehlern und echten Programmierfehlern unterscheidet, sowie optionale strukturierte Metadaten.
Eine wichtige Design Entscheidung ist, ob DomainError abstrakt sein soll. In TypeScript verhindert eine abstract Klasse, dass Entwickler versehentlich new DomainError(...) direkt instanziieren, statt eine der spezifischeren Unterklassen zu verwenden. Diese Einschraenkung erzwingt, dass jeder tatsaechlich geworfene Fehler einer konkreten, benannten Kategorie angehoert, was spaeter sowohl beim Logging als auch bei der Fehlerbehandlung im Aufrufer von Vorteil ist.
Im Konstruktor der Basisklasse ist ein Detail besonders wichtig: Object.setPrototypeOf(this, new.target.prototype) muss nach dem super() Aufruf gesetzt werden, wenn die Klasse ueber mehrere Ebenen von Error erbt und in einer Umgebung laeuft, die Error intern nicht vollstaendig ES6 konform transpiliert, etwa aeltere TypeScript Target Einstellungen wie ES5. Ohne diese Korrektur funktionieren instanceof Pruefungen auf abgeleiteten Klassen in solchen Umgebungen nicht zuverlaessig.
// Base class for the entire domain error hierarchy
abstract class DomainError extends Error {
abstract readonly code: string;
readonly isOperational: boolean;
readonly timestamp: Date;
constructor(message: string, isOperational = true) {
super(message);
this.name = new.target.name;
this.isOperational = isOperational;
this.timestamp = new Date();
// Required for correct instanceof checks when targeting ES5
Object.setPrototypeOf(this, new.target.prototype);
// Node.js: excludes the constructor call itself from the stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, new.target);
}
}
}
class NotFoundError extends DomainError {
readonly code = "NOT_FOUND";
constructor(readonly resourceType: string, readonly resourceId: string) {
super(`${resourceType} with id ${resourceId} was not found`);
}
}
const error = new NotFoundError("Product", "sku-42");
console.log(error instanceof DomainError); // true
console.log(error instanceof Error); // true
console.log(error.code); // "NOT_FOUND"
3. Error Codes und Metadata typsicher anhaengen
Ein reiner code: string reicht fuer viele Faelle, aber die Typsicherheit steigt deutlich, wenn code als Literal Type deklariert wird statt als generischer string. Wird jede konkrete Fehlerklasse in der Error Klassen Hierarchie mit einem festen, literal typisierten code ausgestattet, kann eine Union aller moeglichen Codes automatisch abgeleitet werden, was Autovervollstaendigung und Exhaustiveness Checking bei der Fehlerbehandlung ermoeglicht.
Fuer zusaetzliche Metadaten, etwa welche Felder bei einem Validierungsfehler betroffen sind, empfiehlt sich ein generischer Ansatz in der Basisklasse: ein optionales details Feld, dessen konkreter Typ von der jeweiligen Unterklasse bestimmt wird. So kann ValidationError ein details: { field: string; constraint: string }[] tragen, waehrend RateLimitError stattdessen details: { retryAfterSeconds: number } traegt, jeweils passend zur konkreten Fehlerart.
Ein haeufig genutztes Muster in produktionsreifen Codebasen ist, jeder Fehlerklasse zusaetzlich eine statische httpStatus Eigenschaft mitzugeben, die bestimmt, welcher HTTP Statuscode zurueckgegeben werden soll, wenn der Fehler an der API Grenze in eine Antwort uebersetzt wird. Diese Kopplung an der Quelle, statt in einer separaten Mapping Tabelle irgendwo im Code, reduziert die Wahrscheinlichkeit, dass eine neue Fehlerklasse vergessen wird, wenn das Mapping gepflegt werden muss.
// Error codes as literal types, plus structured metadata per subclass
abstract class DomainError extends Error {
abstract readonly code: string;
abstract readonly httpStatus: number;
}
class ValidationError extends DomainError {
readonly code = "VALIDATION_ERROR" as const;
readonly httpStatus = 422;
constructor(readonly details: Array<{ field: string; constraint: string }>) {
super(`Validation failed for ${details.length} field(s)`);
}
}
class RateLimitError extends DomainError {
readonly code = "RATE_LIMIT_EXCEEDED" as const;
readonly httpStatus = 429;
constructor(readonly retryAfterSeconds: number) {
super(`Rate limit exceeded, retry after ${retryAfterSeconds}s`);
}
}
// Union of all known error codes, derived from the concrete classes
type KnownErrorCode = ValidationError["code"] | RateLimitError["code"];
function logErrorCode(code: KnownErrorCode): void {
console.log(`Handling known error code: ${code}`);
}
4. instanceof Pruefungen und die Prototype Chain richtig behandeln
instanceof ist der zentrale Mechanismus, um eine Error Klassen Hierarchie zur Laufzeit zu unterscheiden, aber er hat mehrere Fallstricke, die insbesondere in gemischten Build Umgebungen auftreten. Der haeufigste Fallstrick ist ein TypeScript Target unterhalb von ES2015, bei dem der generierte JavaScript Code die Prototype Chain fuer Klassen, die von Error erben, nicht korrekt aufbaut. Das im vorherigen Abschnitt gezeigte Object.setPrototypeOf Muster behebt genau dieses Problem.
Ein zweiter Fallstrick betrifft Module Duplikate: Wenn eine Anwendung durch einen Bundler oder ein Monorepo Setup versehentlich zwei separate Kopien derselben Fehlerklasse laedt, etwa weil zwei unterschiedliche Versionen einer Bibliothek gleichzeitig im node_modules Baum existieren, schlaegt instanceof fehl, obwohl der Fehler inhaltlich exakt derselben Klasse entspricht. Dieses Problem ist kein TypeScript spezifisches Problem, sondern eine allgemeine JavaScript Modul Eigenheit, die aber in TypeScript Projekten mit komplexen Monorepo Strukturen besonders haeufig auftritt.
Als robustere Alternative zu reinem instanceof setzen manche Teams zusaetzlich auf eine Kombination aus instanceof und einer Ueberpruefung des code Feldes: error instanceof DomainError && error.code === "NOT_FOUND". Diese doppelte Pruefung bleibt auch dann korrekt, wenn instanceof durch ein Modul Duplikat Problem fehlschlaegt, solange code als reiner Datenwert ueber die Modulgrenze hinweg identisch bleibt.
// Robust checking: instanceof combined with the discriminating code field
function isKnownDomainError(caught: unknown): caught is DomainError {
return (
caught instanceof DomainError ||
// Fallback for duplicate module instances: check shape instead of identity
(typeof caught === "object" &&
caught !== null &&
"code" in caught &&
"isOperational" in caught)
);
}
function handleCaughtError(caught: unknown): void {
if (caught instanceof NotFoundError) {
console.log(`Not found: ${caught.resourceType}/${caught.resourceId}`);
return;
}
if (isKnownDomainError(caught)) {
console.log(`Known domain error: ${caught.code}`);
return;
}
console.error("Unexpected error", caught);
throw caught; // re-throw truly unknown errors, do not swallow them
}
5. Error Hierarchien fuer unterschiedliche Fehlerkategorien
Eine flache Liste aus zwanzig direkt von DomainError erbenden Klassen wird schnell unuebersichtlich. Sinnvoller ist eine mehrstufige Error Klassen Hierarchie, bei der eine mittlere Schicht grobe Kategorien wie ValidationError, NotFoundError Basis, AuthorizationError Basis und ExternalServiceError Basis einfuehrt, von denen wiederum konkrete, spezifische Fehlerklassen erben. Diese Zwischenschicht erlaubt es, mit einer einzigen instanceof Pruefung auf die gesamte Kategorie zu reagieren, ohne jede konkrete Unterklasse einzeln aufzaehlen zu muessen.
Ein praktisches Beispiel: Ein globaler Express Error Handler kann mit if (error instanceof AuthorizationError) pauschal auf HTTP 401 oder 403 reagieren, unabhaengig davon, ob es sich um MissingTokenError, ExpiredTokenError oder InsufficientRoleError handelt. Jede dieser konkreten Klassen kann trotzdem eigene, spezifische Metadaten tragen, die in einem detaillierteren Log Eintrag oder einer spezifischeren Client Fehlermeldung genutzt werden, ohne die grobe Kategorisierung im Handler aufzubrechen.
Die Tiefe der Hierarchie sollte sich an tatsaechlichem Verhalten orientieren, nicht an rein begrifflicher Aehnlichkeit. Zwei Fehlerklassen sollten nur dann eine gemeinsame Zwischenklasse teilen, wenn es tatsaechlich Code gibt, der beide Fehlerarten identisch behandeln will. Eine zu tiefe, rein taxonomische Hierarchie ohne echten Verhaltensbezug erschwert die Wartbarkeit eher, als sie zu verbessern.
// Intermediate category classes group related, specific error classes
abstract class AuthorizationError extends DomainError {
readonly httpStatus = 403;
}
class MissingTokenError extends AuthorizationError {
readonly code = "MISSING_TOKEN" as const;
constructor() {
super("No authentication token was provided");
}
}
class InsufficientRoleError extends AuthorizationError {
readonly code = "INSUFFICIENT_ROLE" as const;
constructor(readonly requiredRole: string) {
super(`Role "${requiredRole}" is required for this operation`);
}
}
// A single check handles the whole category, regardless of the concrete class
function expressErrorHandler(error: unknown): { status: number; body: object } {
if (error instanceof AuthorizationError) {
return { status: error.httpStatus, body: { code: error.code, message: error.message } };
}
if (error instanceof DomainError) {
return { status: error.httpStatus, body: { code: error.code, message: error.message } };
}
return { status: 500, body: { code: "INTERNAL_ERROR", message: "Unexpected error" } };
}
6. Error Serialisierung fuer Logging und API Responses
Eine Error Klassen Hierarchie, die nur intern innerhalb eines Prozesses lebt, ist nur die halbe Loesung. Sobald ein Fehler geloggt, an einen Log Aggregator wie Elasticsearch geschickt, oder als JSON an einen Client zurueckgegeben werden muss, wird deutlich, dass JSON.stringify(error) auf einer nativen Error Instanz standardmaessig ein leeres Objekt {} erzeugt, weil message und stack als nicht enumerierbare Eigenschaften definiert sind. Eine durchdachte Error Klassen Hierarchie muss diese Serialisierung explizit loesen.
Die uebliche Loesung ist eine toJSON() Methode auf der Basisklasse, die explizit alle relevanten Felder in ein enumeriertes Objekt kopiert: code, message, timestamp und optional details. Der Stacktrace sollte in Produktionsumgebungen bewusst nicht standardmaessig in Client Antworten landen, da er interne Dateipfade und Codestruktur offenlegt, aber sehr wohl in internen Log Eintraegen, die nur fuer das eigene Team sichtbar sind.
Fuer strukturierte Logging Frameworks wie Pino oder Winston lohnt es sich, zusaetzlich eine dedizierte toLogObject() Methode zu definieren, die mehr Details enthaelt als toJSON(), etwa den vollstaendigen Stacktrace und interne Debug Informationen, die niemals an den Client gehen sollten. Diese Trennung zwischen Client sicherer und intern vollstaendiger Serialisierung ist ein zentraler Baustein jeder produktionstauglichen Error Klassen Hierarchie.
// Explicit serialization: safe for clients, richer for internal logs
abstract class DomainError extends Error {
abstract readonly code: string;
abstract readonly httpStatus: number;
readonly timestamp = new Date();
toJSON(): Record<string, unknown> {
return {
code: this.code,
message: this.message,
timestamp: this.timestamp.toISOString(),
};
}
toLogObject(): Record<string, unknown> {
return {
...this.toJSON(),
name: this.name,
stack: this.stack,
};
}
}
const error = new NotFoundError("Product", "sku-42");
console.log(JSON.stringify(error)); // uses toJSON automatically
logger.error(error.toLogObject());
const logger = { error: (_payload: Record<string, unknown>): void => {} };
7. Error Klassen mit Cause Chaining verketten
In mehrschichtigen Anwendungen entsteht ein Fehler haeufig in einer tiefen Schicht, etwa einem Datenbank Treiber, wird aber in einer hoeheren Schicht in eine domain spezifische Fehlerklasse uebersetzt. Ohne Cause Chaining geht dabei die urspruengliche, technische Fehlerursache verloren, was Debugging erheblich erschwert. Seit ES2022 unterstuetzt die native Error Klasse ein cause Feld im Konstruktor Options Objekt, das TypeScript ab Version 4.6 vollstaendig typisiert.
Das cause Feld erlaubt, eine domain spezifische Fehlerklasse zu werfen, die den urspruenglichen, technischen Fehler als Ursache referenziert, ohne die Fehlerinformation der unteren Schicht zu verlieren. Beim Loggen kann dann die gesamte Kette von error ueber error.cause bis zur eigentlichen Wurzelursache rekursiv ausgegeben werden, was besonders bei Datenbankfehlern, Netzwerkfehlern oder Fehlern in Drittanbieter Bibliotheken wertvoll ist.
Ein wichtiger Designaspekt: Die Error Klassen Hierarchie sollte cause konsequent in der Basisklasse unterstuetzen, nicht nur in einzelnen Unterklassen, damit jede abgeleitete Fehlerklasse automatisch von diesem Mechanismus profitiert. Eine kleine Utility Funktion, die die gesamte Cause Kette rekursiv in ein Array aus Log Objekten uebersetzt, macht diese Information fuer strukturiertes Logging direkt nutzbar.
// Cause chaining preserves the original low-level error
class DatabaseConnectionError extends DomainError {
readonly code = "DB_CONNECTION_ERROR" as const;
readonly httpStatus = 503;
}
async function loadUserProfile(userId: string): Promise<unknown> {
try {
return await queryDatabase(userId);
} catch (rawError) {
// The original driver error is preserved via the cause option
throw new DatabaseConnectionError("Failed to load user profile", { cause: rawError });
}
}
function collectCauseChain(error: unknown): string[] {
const messages: string[] = [];
let current: unknown = error;
while (current instanceof Error) {
messages.push(current.message);
current = current.cause;
}
return messages;
}
async function queryDatabase(_userId: string): Promise<unknown> {
return {};
}
8. Typische Fehler beim Bauen von Error Hierarchien
Der haeufigste Fehler ist, Object.setPrototypeOf beziehungsweise ein modernes TypeScript Target zu vergessen, wodurch instanceof Pruefungen auf abgeleiteten Klassen stillschweigend fehlschlagen. Dieser Bug faellt oft erst spaet auf, weil die Fehlerklasse in einfachen Tests korrekt funktioniert, aber in einer produktiven Build Pipeline mit einem aelteren Compile Target ploetzlich nicht mehr erkannt wird.
// WRONG: missing prototype fix breaks instanceof on older compile targets
class BrokenError extends Error {
constructor(message: string) {
super(message);
// Missing: Object.setPrototypeOf(this, BrokenError.prototype);
}
}
// WRONG: swallowing the cause instead of chaining it
async function badFetch(): Promise<void> {
try {
await externalCall();
} catch {
throw new Error("External call failed"); // original cause is lost
}
}
// RIGHT: always fix the prototype chain and preserve the cause
class FixedError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, FixedError.prototype);
}
}
async function goodFetch(): Promise<void> {
try {
await externalCall();
} catch (rawError) {
throw new Error("External call failed", { cause: rawError });
}
}
async function externalCall(): Promise<void> {}
Ein zweiter verbreiteter Fehler ist eine zu flache Hierarchie ohne Kategorisierung, bei der jede Fehlerklasse direkt von Error erbt, ohne eine gemeinsame Basisklasse. Das fuehrt dazu, dass jede Konsumentenstelle im Code jede konkrete Fehlerklasse einzeln importieren und pruefen muss, statt mit einer einzigen instanceof DomainError Pruefung eine ganze Kategorie zu erfassen. Ein dritter Fehler ist, isOperational oder ein aehnliches Flag ganz wegzulassen, wodurch echte Programmierfehler nicht mehr von erwartbaren Geschaeftslogik Fehlern unterschieden werden koennen, was insbesondere fuer Crash Reporting und Prozessueberwachung wichtig ist.
9. Ansaetze im Vergleich
Die folgende Tabelle vergleicht unterschiedliche Ansaetze zur Fehlerklassenmodellierung.
| Kriterium | new Error(message) | Flache eigene Klassen | Error Klassen Hierarchie |
|---|---|---|---|
| Maschinenlesbare Codes | Nein | Ja, pro Klasse | Ja, konsistent ueber Basisklasse |
| Kategorisierte instanceof Checks | Nein | Nein, jede Klasse einzeln | Ja, ueber Zwischenklassen |
| Serialisierung fuer Logs/API | Manuell noetig | Uneinheitlich pro Klasse | Konsistent ueber toJSON |
| Cause Chaining | Manuell moeglich | Manuell moeglich | Standardisiert in Basisklasse |
| Wartbarkeit im Team | Gering | Mittel | Hoch |
Fuer kleine Skripte reicht new Error(message) aus. Sobald jedoch mehrere Entwickler an unterschiedlichen Fehlerarten arbeiten und diese konsistent geloggt, kategorisiert und an Clients zurueckgegeben werden sollen, zahlt sich der anfaengliche Aufwand einer vollstaendigen Error Klassen Hierarchie deutlich aus.
Mironsoft
Backend Architektur, Logging und Fehlerbehandlung fuer Node.js
Eine Error Hierarchie, die euer ganzes Team versteht?
Wir entwerfen eine vollstaendige Error Klassen Hierarchie mit Error Codes, Serialisierung und Cause Chaining fuer eure Node.js und TypeScript Backends.
Hierarchie Design
Basisklasse, Kategorien und Error Codes fuer euer Backend entwerfen
Logging Integration
Strukturierte Serialisierung fuer Pino, Winston und Elasticsearch
Migration
Bestehende new Error Aufrufe schrittweise auf die Hierarchie umstellen
10. Zusammenfassung
Eine gut strukturierte Error Klassen Hierarchie ersetzt verstreute new Error("...") Aufrufe durch eine gemeinsame Basisklasse mit Error Codes, strukturierten Metadaten und konsistenter Serialisierung. Eine abstrakte Basisklasse verhindert versehentliche direkte Instanziierung, waehrend Object.setPrototypeOf und moderne Compile Targets sicherstellen, dass instanceof Pruefungen auf abgeleiteten Klassen zuverlaessig funktionieren.
Zwischenschichten fuer Kategorien wie AuthorizationError erlauben pauschale Fehlerbehandlung, ohne jede konkrete Unterklasse einzeln aufzuzaehlen. toJSON() und toLogObject() trennen client sichere von intern vollstaendiger Serialisierung, und cause Chaining bewahrt die urspruengliche technische Fehlerursache ueber mehrere Schichten hinweg. Zusammen ergibt das eine Error Klassen Hierarchie, die sowohl Debugging als auch Fehlerbehandlung im gesamten Team spuerbar erleichtert.
Error Klassen Hierarchien in TypeScript, das Wichtigste auf einen Blick
Basisklasse
Abstrakte DomainError Klasse mit Error Code, isOperational Flag und Timestamp fuer alle Fehlerarten.
instanceof Sicherheit
Object.setPrototypeOf nach super() garantiert korrekte instanceof Pruefungen auch bei aelteren Compile Targets.
Serialisierung
toJSON fuer Client Antworten, toLogObject mit vollem Stacktrace fuer interne Logs.
Cause Chaining
Das native cause Feld bewahrt die urspruengliche technische Fehlerursache ueber mehrere Schichten.