Result Type Pattern in TypeScript: Fehlerbehandlung ohne Exceptions
AI generated
<T>
type
TypeScript · Fehlerbehandlung · Funktionale Patterns
Result Type Pattern in TypeScript
Fehlerbehandlung ohne versteckte Exceptions

Exceptions in TypeScript sind unsichtbar fuer den Compiler: eine Funktion, die wirft, sieht in ihrer Signatur genauso aus wie eine, die niemals scheitert. Das Result Type Pattern macht Fehlerpfade explizit, indem Erfolg und Fehlschlag als typisierter Wert zurueckgegeben werden, den der Aufrufer aktiv behandeln muss.

18 Min. Lesezeit Ok · Err · map · andThen · unwrap TypeScript 5.x

1. Warum Exceptions in TypeScript ein Problem sind

TypeScript erweitert JavaScript um ein statisches Typsystem, aber das Ausnahmesystem bleibt vollstaendig ausserhalb dieses Systems. Eine Funktion mit der Signatur function parseConfig(input: string): Config gibt vor, immer eine Config zu liefern, kann aber intern eine Exception werfen und den Kontrollfluss verlassen, ohne dass der Compiler davon irgendetwas weiss. Das Result Type Pattern setzt genau hier an: es macht Fehlschlaege zu einem Teil des Rueckgabetyps, sodass der Aufrufer sie im Typsystem sieht und nicht ignorieren kann.

Das Problem verschaerft sich in groesseren Codebasen. Wer eine Funktion aufruft, die drei Ebenen tiefer eine Exception wirft, muss dokumentiert wissen, dass diese Exception existiert. TypeScript bietet dafuer kein Sprachmittel wie checked exceptions aus Java, JSDoc Kommentare mit @throws werden nicht vom Compiler geprueft. Ein Result Type loest dieses Dokumentationsproblem, weil der Fehlerfall direkt im Funktionssignatur sichtbar ist und jede Verwendungsstelle vom Compiler gezwungen wird, beide Faelle zu behandeln.

Ein weiterer Aspekt betrifft die Performance und Vorhersagbarkeit des Kontrollflusses. Exceptions in JavaScript sind fuer echte Ausnahmefaelle gedacht, nicht fuer erwartbare Fehlschlaege wie eine ungueltige Benutzereingabe oder einen fehlgeschlagenen Netzwerkaufruf. Wer Validierungsfehler ueber Exceptions modelliert, vermischt zwei unterschiedliche Kategorien: echte Programmierfehler und erwartbare Geschaeftslogik Ergebnisse. Das Result Type Pattern trennt diese sauber: Programmierfehler duerfen weiterhin werfen, erwartbare Fehlschlaege werden als Wert zurueckgegeben.

2. Das Result Type Grundprinzip: Ok und Err

Im Kern ist ein Result Type eine Discriminated Union aus zwei Varianten: einem Erfolgsfall, ueblicherweise Ok genannt, und einem Fehlerfall, ueblicherweise Err genannt. Beide Varianten teilen ein gemeinsames Diskriminator Feld, meist success oder ok, ueber das TypeScript den konkreten Typ zur Laufzeit unterscheiden kann. Diese Idee stammt urspruenglich aus funktionalen Sprachen wie Rust und Haskell, wo Result<T, E> beziehungsweise Either ein zentrales Sprachmittel fuer Fehlerbehandlung sind.

Der entscheidende Unterschied zu einem einfachen { data: T | null, error: E | null } Objekt liegt in der Exhaustiveness. Bei einem Result Type als echte Union kann niemals gleichzeitig ein Wert und ein Fehler vorliegen, und der Compiler erzwingt eine Fallunterscheidung, bevor auf value zugegriffen werden darf. Bei einem losen Objekt mit zwei optionalen Feldern gibt es hingegen theoretisch vier Zustaende, von denen zwei unsinnig sind, aber vom Typsystem nicht ausgeschlossen werden.

In der Praxis begegnet man dem Result Type haeufig in Bibliotheken wie neverthrow oder ts-results, die eine ausgereifte Implementierung mit Hilfsmethoden mitbringen. Fuer viele Projekte reicht aber auch eine minimale Eigenimplementierung, die genau auf den eigenen Anwendungsfall zugeschnitten ist und keine zusaetzliche Abhaengigkeit einfuehrt. Der folgende Abschnitt zeigt, wie ein solcher generischer Result Type von Grund auf aussieht.


// Core Result type: a discriminated union of Ok and Err
type Ok<T> = { readonly ok: true; readonly value: T };
type Err<E> = { readonly ok: false; readonly error: E };
type Result<T, E> = Ok<T> | Err<E>;

// Constructor functions keep call sites readable
function ok<T>(value: T): Ok<T> {
  return { ok: true, value };
}

function err<E>(error: E): Err<E> {
  return { ok: false, error };
}

// Usage: the return type documents both outcomes
function parseAge(input: string): Result<number, string> {
  const parsed = Number(input);
  if (Number.isNaN(parsed) || parsed < 0) {
    return err(`"${input}" is not a valid age`);
  }
  return ok(parsed);
}

const result = parseAge("42");
if (result.ok) {
  console.log(result.value); // narrowed to number
} else {
  console.error(result.error); // narrowed to string
}

3. Einen generischen Result Type implementieren

Die minimale Definition aus dem vorherigen Abschnitt reicht fuer einfache Faelle, aber ein produktionstauglicher Result Type braucht zusaetzliche Hilfsfunktionen, die typische Operationen kapseln. Dazu gehoert eine sichere Methode, um den Wert zu extrahieren, ohne manuell auf ok zu pruefen, sowie eine Methode, die im Fehlerfall einen Standardwert liefert. Diese Funktionen werden meist als eigenstaendige Utility Funktionen implementiert, nicht als Klassenmethoden, um die Werte weiterhin als reine, serialisierbare Objekte zu behandeln.

Ein wichtiges Detail bei der Implementierung ist die Namensgebung der generischen Parameter. Result<T, E> folgt der Konvention aus Rust, wobei E haeufig auf string, ein eigenes Error Objekt oder eine Discriminated Union von Fehlerarten gesetzt wird. Wer den Fehlertyp auf string beschraenkt, verliert Struktur, gewinnt aber Einfachheit. Wer stattdessen eine eigene Fehler Union verwendet, kann im Aufrufer per switch auf konkrete Fehlerarten reagieren, was insbesondere bei mehreren moeglichen Fehlerquellen sinnvoll ist.

Ein weiterer haeufig uebersehener Punkt: die Konstruktorfunktionen ok und err sollten generisch genug bleiben, um auch mit void als Erfolgswert zu funktionieren, etwa bei einer Funktion, die nur eine Nebenwirkung ausfuehrt und im Erfolgsfall nichts Sinnvolles zurueckgibt. TypeScript leitet in diesem Fall Ok<void> automatisch ab, wenn ok() ohne Argument aufgerufen wird und der generische Parameter entsprechend beschraenkt ist.


// Utility functions built on top of the core Result type
function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
  return result.ok;
}

function isErr<T, E>(result: Result<T, E>): result is Err<E> {
  return !result.ok;
}

// Safe extraction with a fallback value, never throws
function unwrapOr<T, E>(result: Result<T, E>, fallback: T): T {
  return result.ok ? result.value : fallback;
}

// Extraction that throws only when the caller explicitly asks for it
function unwrap<T, E>(result: Result<T, E>): T {
  if (result.ok) {
    return result.value;
  }
  throw new Error(`Called unwrap on an Err value: ${JSON.stringify(result.error)}`);
}

// A domain-specific error union instead of a plain string
type ValidationError =
  | { kind: "empty"; field: string }
  | { kind: "tooShort"; field: string; minLength: number }
  | { kind: "invalidFormat"; field: string };

function validateUsername(input: string): Result<string, ValidationError> {
  if (input.length === 0) {
    return err({ kind: "empty", field: "username" });
  }
  if (input.length < 3) {
    return err({ kind: "tooShort", field: "username", minLength: 3 });
  }
  return ok(input);
}

4. Result Type mit map, andThen und unwrap verketten

Der eigentliche Mehrwert des Result Type Pattern zeigt sich, sobald mehrere fehleranfaellige Operationen nacheinander ausgefuehrt werden muessen. Ohne Hilfsfunktionen entsteht schnell eine tief verschachtelte Kette von if (result.ok) Bloecken, die die Lesbarkeit stark beeintraechtigt. Die Methode map transformiert den Erfolgswert, ohne den Fehlerfall anzufassen, waehrend andThen, manchmal auch flatMap genannt, eine weitere Operation anhaengt, die selbst wieder einen Result Type zurueckgibt.

Diese Verkettung folgt demselben Prinzip wie Promise.then, nur synchron und ohne Microtask Queue. Sobald irgendein Schritt in der Kette einen Err zurueckgibt, werden alle nachfolgenden map und andThen Aufrufe uebersprungen und der urspruengliche Fehler wird bis zum Ende der Kette durchgereicht. Das entspricht dem Verhalten, das man von Exceptions kennt, bleibt aber vollstaendig im Typsystem sichtbar und erzwingt am Ende eine explizite Behandlung.

In der Praxis lohnt es sich, diese Kombinatoren als Methoden auf einem kleinen Wrapper Objekt bereitzustellen, statt als freistehende Funktionen, um die Lesbarkeit von Ketten zu verbessern. Bibliotheken wie neverthrow bieten genau das: ein Result Objekt mit chainbaren Methoden, das sich fast wie eine Promise Kette liest, aber synchron und ohne verstecktes Werfen bleibt.


// Chaining combinators for the Result type
function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {
  return result.ok ? ok(fn(result.value)) : result;
}

function andThen<T, U, E>(
  result: Result<T, E>,
  fn: (value: T) => Result<U, E>
): Result<U, E> {
  return result.ok ? fn(result.value) : result;
}

function mapErr<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> {
  return result.ok ? result : err(fn(result.error));
}

// A realistic pipeline: parse, validate, normalize
function parsePrice(raw: string): Result<number, string> {
  const value = Number(raw.replace(",", "."));
  return Number.isNaN(value) ? err("Price is not a number") : ok(value);
}

function ensurePositive(value: number): Result<number, string> {
  return value > 0 ? ok(value) : err("Price must be positive");
}

function roundToCents(value: number): number {
  return Math.round(value * 100) / 100;
}

const priceResult = andThen(
  andThen(parsePrice("19,99"), ensurePositive),
  (value) => ok(roundToCents(value))
);

if (priceResult.ok) {
  console.log(`Final price: ${priceResult.value}`);
} else {
  console.error(`Pipeline failed: ${priceResult.error}`);
}

5. Result Type in async Funktionen mit Promise kombinieren

Asynchrone Operationen sind der haeufigste Ort, an dem Exceptions in TypeScript unsichtbar bleiben. Ein fetch Aufruf kann aus vielen Gruenden scheitern: Netzwerkfehler, Timeout, HTTP Statuscodes ausserhalb des Erfolgsbereichs. Ohne Result Type muss jede aufrufende Funktion wissen, dass sie await in einen try/catch Block einbetten muss, sonst propagiert die Exception unbehandelt durch die gesamte Aufruferkette.

Die Loesung ist ein ResultAsync Typ, im Grunde ein Promise<Result<T, E>>, der niemals reject wird. Jede asynchrone Funktion, die intern werfen koennte, wird in einer Wrapper Funktion abgefangen und das Ergebnis in ok oder err uebersetzt. Der Aufrufer awaited dann ein Promise, das garantiert erfuellt wird, und behandelt den Fehlerfall anschliessend synchron ueber die Discriminated Union.

Ein haeufiges Missverstaendnis ist, dass ein Result Type Exceptions komplett ersetzen muesste. In der Praxis bleibt eine aeussere Schutzschicht mit try/catch sinnvoll, etwa an der Grenze zu Framework Code, der selbst wirft, oder um wirklich unerwartete Programmierfehler wie einen Null Pointer abzufangen, bevor die gesamte Anwendung abstuerzt. Der Result Type deckt die erwartbaren, dokumentierten Fehlerfaelle ab, waehrend echte Exceptions fuer die unerwarteten Ausnahmen reserviert bleiben.


// Wrapping a throwing async operation into a safe Result
type ResultAsync<T, E> = Promise<Result<T, E>>;

async function fetchJson<T>(url: string): ResultAsync<T, string> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      return err(`HTTP ${response.status} for ${url}`);
    }
    const data = (await response.json()) as T;
    return ok(data);
  } catch (cause) {
    // Network errors, JSON parse errors, and CORS failures land here
    return err(`Request to ${url} failed: ${String(cause)}`);
  }
}

interface Product {
  id: number;
  name: string;
  price: number;
}

async function loadProduct(id: number): ResultAsync<Product, string> {
  const result = await fetchJson<Product>(`/api/products/${id}`);
  return map(result, (product) => ({
    ...product,
    price: roundToCents(product.price),
  }));
}

// Call site: no try/catch needed, the caller always gets a Result
const productResult = await loadProduct(42);
if (productResult.ok) {
  renderProduct(productResult.value);
} else {
  showErrorBanner(productResult.error);
}

function renderProduct(product: Product): void {}
function showErrorBanner(message: string): void {}

6. Result Type in der Praxis: API Calls typsicher behandeln

In Magento und Hyvae Projekten begegnet man haeufig GraphQL oder REST Aufrufen im Frontend, deren Fehlerbehandlung sich stark verbessert, wenn sie konsequent auf dem Result Type Pattern aufbaut. Statt jede Komponente einzeln mit try/catch auszustatten, kapselt eine zentrale API Schicht alle Netzwerkoperationen und liefert ausschliesslich Result Type Werte nach aussen. Die aufrufende Komponente muss dann nur noch die beiden Faelle behandeln, ohne die internen Fehlerdetails der HTTP Schicht zu kennen.

Ein praktisches Muster: Formularvalidierung und Server Antwort werden ueber denselben Result Type modelliert. Der Client validiert lokal mit einem Result<FormData, ValidationError[]>, bevor ueberhaupt ein Request abgeschickt wird. Kommt die Antwort vom Server zurueck, wird sie in denselben Fehlertyp uebersetzt, sodass die UI Komponente, die Fehlermeldungen anzeigt, nicht unterscheiden muss, ob der Fehler lokal oder serverseitig entstanden ist.

Bei Checkout Prozessen oder Zahlungsabwicklung ist diese Explizitheit besonders wertvoll, weil ein uebersehener Fehlerfall dort direkte finanzielle Konsequenzen hat. Ein Result Type, der vom Compiler erzwungen behandelt werden muss, verhindert genau die Klasse von Bugs, bei denen eine fehlgeschlagene Zahlung stillschweigend als Erfolg dargestellt wird, weil irgendwo im Code ein catch Block vergessen wurde.

7. Result Type versus try/catch: wann sich was lohnt

Das Result Type Pattern ist kein universeller Ersatz fuer try/catch, sondern ein zusaetzliches Werkzeug fuer eine bestimmte Kategorie von Fehlern. Fuer erwartbare, in der Domaene vorhersehbare Fehlschlaege wie ungueltige Eingaben, fehlgeschlagene Validierung oder erwartete HTTP Fehlercodes ist ein Result Type die bessere Wahl, weil er den Fehlerfall im Typsystem sichtbar macht und Vollstaendigkeit erzwingt.

Fuer echte Ausnahmefaelle, also Situationen, die per Definition nicht im normalen Kontrollfluss vorkommen sollten, etwa ein korrupter interner Zustand oder ein Programmierfehler, bleibt throw sinnvoll. Diese Faelle sollen die Anwendung tatsaechlich unterbrechen, nicht als Datenwert weitergereicht werden, denn eine stille Fortsetzung nach einem inkonsistenten Zustand ist oft gefaehrlicher als ein klarer Absturz mit Stacktrace.

Ein pragmatischer Richtwert: Wenn ein Fehlerfall in der Dokumentation der Funktion erwaehnenswert waere, etwa @throws in einem JSDoc Kommentar, dann ist er wahrscheinlich ein guter Kandidat fuer einen Result Type. Wenn der Fehlerfall so unerwartet ist, dass er nirgendwo dokumentiert wuerde, weil er theoretisch nie eintreten sollte, bleibt eine Exception die passendere Wahl.

8. Typische Fehler beim Einsatz von Result Types

Der haeufigste Fehler ist, den Result Type nur an einer Stelle einzufuehren und die aufrufende Kette weiterhin mit ungeprueften Zugriffen zu durchsetzen. Ein result.value Zugriff ohne vorherige Pruefung von result.ok funktioniert nur, wenn TypeScript im Strict Mode laeuft und die Union tatsaechlich narrowing erzwingt. Ohne konsequente Pruefung im gesamten Aufrufpfad geht der Vorteil der Typsicherheit verloren, weil ein einzelner unpassend gecasteter Zugriff das gesamte Sicherheitsnetz durchbricht.


// WRONG: bypassing the discriminated union with a type assertion
function unsafeGetValue<T, E>(result: Result<T, E>): T {
  return (result as Ok<T>).value; // compiles, but crashes at runtime on Err
}

// WRONG: swallowing the error instead of propagating it
function badHandling(result: Result<number, string>): number {
  if (!result.ok) {
    return 0; // silently hides the failure, caller cannot distinguish
  }
  return result.value;
}

// RIGHT: force the caller to decide what a missing value means
function safeHandling(result: Result<number, string>): Result<number, string> {
  return result; // propagate unchanged, let the ultimate caller decide
}

// RIGHT: exhaustiveness check with a switch, no assertion needed
function describe<T>(result: Result<T, string>): string {
  switch (result.ok) {
    case true:
      return `Success: ${JSON.stringify(result.value)}`;
    case false:
      return `Failure: ${result.error}`;
  }
}

Ein weiterer verbreiteter Fehler ist die Vermischung von Result Type und Exceptions innerhalb derselben Funktion. Wenn eine Funktion sowohl einen Err zurueckgeben als auch werfen kann, muss der Aufrufer beide Mechanismen gleichzeitig behandeln, was den gesamten Vorteil des Patterns zunichte macht. Konsistenz ist hier wichtiger als Perfektion: Eine Funktion sollte entweder komplett auf Result Type setzen oder komplett auf Exceptions, aber niemals beides innerhalb derselben Signatur mischen.

9. Result Type Pattern im Vergleich

Die Wahl zwischen Exceptions, dem Result Type und einer einfachen Nullable Rueckgabe haengt vom konkreten Anwendungsfall ab. Die folgende Tabelle stellt die drei Ansaetze anhand typischer Kriterien gegenueber.

Kriterium Exception (try/catch) Result Type Nullable Rueckgabe
Sichtbar im Typsystem Nein Ja Teilweise
Fehlerdetails transportierbar Ja, ueber Error Objekt Ja, strukturiert typisiert Nein, nur Praesenz
Compiler erzwingt Behandlung Nein Ja, bei Strict Mode Nur bei striktem Null Check
Verkettbarkeit Umstaendlich map, andThen nativ Nicht vorgesehen
Geeignet fuer echte Ausnahmen Ja Bedingt Nein

In gemischten Codebasen ist es durchaus ueblich, alle drei Ansaetze parallel zu verwenden, solange klare Grenzen definiert sind. Eine bewaehrte Konvention: Domain und Business Logik nutzen konsequent den Result Type, waehrend die aeusserste Schicht, etwa ein globaler Error Handler in Express oder ein Alpine.js Event Listener, verbliebene Exceptions abfaengt und in eine einheitliche Fehleranzeige uebersetzt.

Mironsoft

TypeScript Architektur, typsichere APIs und robuste Fehlerbehandlung

TypeScript Code, der Fehler nicht verschluckt?

Wir bauen typsichere Fehlerbehandlung mit Result Type Pattern, Error Klassen Hierarchien und sauberen API Schichten fuer euren Magento, Hyvae und Node.js Stack.

Architektur Review

Analyse bestehender Fehlerbehandlung und Migrationsplan auf Result Types

API Schicht

Zentrale, typsichere API Wrapper mit ResultAsync fuer euer Frontend

Team Schulung

Workshops zu funktionaler Fehlerbehandlung und TypeScript Patterns

10. Zusammenfassung

Das Result Type Pattern macht Fehlerpfade in TypeScript sichtbar, wo Exceptions unsichtbar bleiben. Statt eine Exception zu werfen, gibt eine Funktion eine Discriminated Union aus Ok und Err zurueck, die der Aufrufer aktiv behandeln muss, bevor er auf den Erfolgswert zugreifen kann. Hilfsfunktionen wie map, andThen und unwrapOr erlauben eine lesbare Verkettung mehrerer fehleranfaelliger Schritte, aehnlich einer Promise Kette, aber synchron und ohne verstecktes Werfen.

In asynchronem Code kapselt ein ResultAsync, im Kern ein niemals ablehnendes Promise, alle Netzwerkfehler in einem typisierten Ergebnis. Wichtig bleibt die klare Abgrenzung: Der Result Type ist fuer erwartbare, dokumentierbare Fehlschlaege gedacht, waehrend echte Ausnahmefaelle weiterhin ueber throw behandelt werden sollten. Wer beide Mechanismen konsequent trennt, gewinnt lesbaren, typsicheren Code, bei dem kein Fehlerfall mehr unbemerkt durchrutscht.

Result Type Pattern in TypeScript, das Wichtigste auf einen Blick

Grundprinzip

Result<T, E> = Ok<T> | Err<E>, eine Discriminated Union, die Fehlerpfade im Typsystem verankert statt sie zu verstecken.

Verkettung

map und andThen verketten fehleranfaellige Schritte, ohne verschachtelte if Bloecke, aehnlich einer Promise Kette.

Async

ResultAsync als niemals ablehnendes Promise kapselt Netzwerkfehler und ersetzt verstreute try/catch Bloecke.

Abgrenzung

Erwartbare Fehler ueber Result Type, echte Programmierfehler weiterhin ueber throw, niemals beides in derselben Funktion mischen.

11. FAQ: Result Type Pattern in TypeScript

1Was ist das Result Type Pattern?
Eine Discriminated Union aus Ok und Err, die Fehlerpfade im Typsystem sichtbar macht statt ueber eine Exception zu werfen.
2Warum reicht try/catch nicht?
TypeScript kennt keine checked exceptions, der Compiler kann eine fehlende Fehlerbehandlung nicht erzwingen.
3Brauche ich neverthrow?
Nein, eine minimale Eigenimplementierung reicht fuer viele Projekte und vermeidet eine zusaetzliche Abhaengigkeit.
4Wie kombiniere ich das mit async/await?
Ueber ResultAsync, ein Promise, das niemals reject wird, weil interne Fehler in einen Err Wert uebersetzt werden.
5Ersetzt das Pattern alle Exceptions?
Nein, echte Ausnahmefaelle wie Programmierfehler sollten weiterhin ueber throw behandelt werden.
6map versus andThen?
map transformiert einen Wert direkt, andThen haengt eine weitere Operation an, die selbst einen Result Type liefert.
7Mehrere Fehlerarten behandeln?
Der Fehlertyp sollte eine eigene Union sein, dann ist per switch eine gezielte Reaktion moeglich.
8Besser als data/error Objekt?
Ja, weil eine echte Union unsinnige Zustaende wie gleichzeitig gesetzten Wert und Fehler strukturell ausschliesst.
9Braucht es Strict Mode?
Fuer maximale Narrowing Sicherheit sollten strictNullChecks und Strict Mode aktiviert sein.
10Wie teste ich Result Type Funktionen?
Tests pruefen Ok- und Err-Zweig direkt am Rueckgabewert, ohne try/catch und ohne Exception Assertion Framework.