Mit try/catch/finally regelt JavaScript Exception-Handling — also den geordneten Umgang mit Laufzeit-Fehlern, die in einem Block auftreten. try markiert den abzusichernden Code, catch fängt geworfene Werte ab, finally läuft in jedem Fall, egal ob ein Fehler entstand oder nicht. Dieser Artikel deckt die Grundlagen ab — die volle Behandlung mit Error-Klassen, Error.cause, AggregateError und Async-Patterns folgt im Error-Handling-Kapitel. Hier geht es um die Mechanik der drei Statements, das Optional Catch Binding (ES2019) und ein paar Stolperer im Control-Flow.

Grundform

Ein try-Block muss mit catch, finally oder beidem kombiniert werden. Beide Klauseln sind erlaubt, nur eine reicht aus.

JavaScript try-grundform.js
function parse(json) {
    try {
        return JSON.parse(json);
    } catch (err) {
        console.log('Parse-Fehler:', err.message);
        return null;
    }
}

console.log(parse('{"ok":true}')); // { ok: true }
console.log(parse('das ist nicht JSON')); // null
Output
{ ok: true }
Parse-Fehler: Unexpected token 'd', "das ist no"... is not valid JSON
null

Der err-Parameter im catch ist der geworfene Wert. Konventionell ist das ein Error-Objekt mit einer .message-Property, aber die Sprache erlaubt jeden Wert.

throw — eigene Fehler werfen

Mit throw löst man eine Exception aus. Der Operand kann jeder Wert sein, idiomatisch ist aber eine Instanz von Error (oder einer Sub-Klasse).

JavaScript throw-grundform.js
function teilen(a, b) {
    if (b === 0) throw new Error('Division durch Null');
    return a / b;
}

try {
    teilen(10, 0);
} catch (err) {
    console.log('Fehler:', err.message);
    console.log('Typ:  ', err.constructor.name);
}
Output
Fehler: Division durch Null
Typ:   Error

JavaScript hat eingebaute Error-Sub-Klassen: TypeError, RangeError, SyntaxError, ReferenceError, URIError, EvalError, AggregateError. Selbst geschriebene Klassen (class MyError extends Error) folgen demselben Pattern. Details im Error-Handling-Kapitel.

Optional Catch Binding (ES2019)

Wenn der Error-Wert im catch gar nicht gebraucht wird, kann der Parameter weggelassen werden — catch {} statt catch (err) {}.

JavaScript optional-catch.js
function hatZahl(str) {
    try {
        Number.parseInt(str);
        return /\d/.test(str);  // Test, ob mindestens eine Ziffer da ist
    } catch {
        // Detail des Fehlers irrelevant — einfach false
        return false;
    }
}

console.log(hatZahl('abc123')); // true
console.log(hatZahl('keine'));  // false
Output
true
false

Praktisch in Situationen, in denen man nur „funktioniert oder nicht?" wissen will und der Fehler selbst keine Rolle spielt. ESLint hat keine generelle Empfehlung — aber in Code, der Fehler-Informationen tatsächlich braucht (Logging, User-Feedback), schreibt man weiterhin catch (err).

finally — läuft immer

Der finally-Block läuft, egal ob der try-Block normal beendet wurde, ein Fehler geworfen wurde, oder per return/break/continue verlassen wurde. Klassischer Anwendungsfall: Aufräumen von Ressourcen.

JavaScript finally-cleanup.js
function mitVerbindung() {
    const conn = oeffnen();
    try {
        return arbeite(conn);
    } finally {
        schliessen(conn);
    }
}

function oeffnen()  { console.log('Open');  return { id: 1 }; }
function arbeite(c) { console.log('Use',  c.id); return 'OK'; }
function schliessen(c) { console.log('Close', c.id); }

console.log('Ergebnis:', mitVerbindung());
Output
Open
Use 1
Close 1
Ergebnis: OK

finally läuft auch, wenn arbeite() einen Fehler wirft — die Connection wird trotzdem geschlossen. Das ist das zentrale Muster für ressource-sicheren Code.

finally mit return — überschreibt das try-return

Wenn finally einen eigenen return enthält, ersetzt er alle anderen Control-Flow-Befehle aus try oder catch — auch Exceptions werden „verschluckt".

JavaScript finally-return-falle.js
function f() {
    try {
        return 'aus try';
    } finally {
        return 'aus finally';   // ÜBERSCHREIBT das andere return
    }
}
console.log(f()); // 'aus finally'

function g() {
    try {
        throw new Error('explodiert');
    } finally {
        return 'unterdrückt';   // verschluckt die Exception
    }
}
console.log(g()); // 'unterdrückt' — kein Fehler propagiert
Output
aus finally
unterdrückt

Das ist fast immer ein Bug. ESLint warnt mit no-unsafe-finally. Regel: kein return, kein break, kein continue, kein throw in finally — nur Cleanup-Code.

Re-throw — Fehler weiterleiten

Manchmal will man einen Fehler abfangen, ihn loggen oder dekorieren, und dann weiterwerfen. Das ist „Re-throw".

JavaScript rethrow.js
function ladeKonfig() {
    try {
        return JSON.parse('das ist kaputt');
    } catch (err) {
        console.log('LOG:', err.message);
        throw err;   // weitergeben
    }
}

try {
    ladeKonfig();
} catch (err) {
    console.log('Aufrufer:', err.message);
}
Output
LOG: Unexpected token 'd', "das ist kaputt" is not valid JSON
Aufrufer: Unexpected token 'd', "das ist kaputt" is not valid JSON

Mit Error.cause (ES2022) lässt sich der ursprüngliche Fehler an einen neuen anhängen — sauber typisierter Re-throw mit Zusatz-Information:

JavaScript rethrow-cause.js
function ladeKonfig() {
    try {
        return JSON.parse('das ist kaputt');
    } catch (err) {
        throw new Error('Konfig konnte nicht geladen werden', { cause: err });
    }
}

try {
    ladeKonfig();
} catch (err) {
    console.log('Fehler:    ', err.message);
    console.log('Ursprung:  ', err.cause.message);
}
Output
Fehler:     Konfig konnte nicht geladen werden
Ursprung:   Unexpected token 'd', "das ist kaputt" is not valid JSON

Verschachtelte try-Blöcke

try-Blöcke lassen sich verschachteln. Ein Inner-try fängt nur die Fehler aus seinem Body — der äußere bekommt nur, was nicht innen behandelt wurde.

JavaScript nested-try.js
try {
    try {
        throw new Error('A');
    } catch (e) {
        console.log('Innen:', e.message);
        throw new Error('B aus inner-catch');
    }
} catch (e) {
    console.log('Aussen:', e.message);
}
Output
Innen: A
Aussen: B aus inner-catch

Sinnvoll, wenn verschiedene Sub-Operationen unabhängige Fehler-Behandlung brauchen — meist aber lesbarer als ausgelagerte Funktionen.

catch fängt ALLES — auch fremde Fehler-Typen

Anders als in Java oder C# gibt es keinen Typ-Filter im catch (catch (TypeError e) existiert nicht). Jeder geworfene Wert wird gefangen — Strings, Numbers, Objects, Errors. Man muss intern selbst typunterscheiden.

JavaScript catch-typ-pruefen.js
function f(x) {
    if (typeof x !== 'number') throw new TypeError('Number erwartet');
    if (x < 0) throw new RangeError('positiv erwartet');
    return Math.sqrt(x);
}

try {
    f('foo');
} catch (err) {
    if (err instanceof TypeError) {
        console.log('Typ-Fehler:', err.message);
    } else if (err instanceof RangeError) {
        console.log('Bereich:', err.message);
    } else {
        throw err;   // unbekannter Fehler — weitergeben
    }
}
Output
Typ-Fehler: Number erwartet

Wichtige Konvention: unbekannte Fehler nicht still verschlucken. Wenn der catch-Block den Fehler-Typ nicht versteht, immer throw err; als Default-Fall.

try/catch in async-Funktionen

In async-Funktionen fängt try/catch auch rejected Promises ab — über await. Detail-Behandlung im Async-Error-Handling-Artikel, hier nur das Grund-Muster.

JavaScript async-try-catch.js
async function lade() {
    throw new Error('Server tot');
}

async function main() {
    try {
        await lade();
    } catch (err) {
        console.log('Async-Fehler:', err.message);
    } finally {
        console.log('Aufräumen');
    }
}

main();
Output
Async-Fehler: Server tot
Aufräumen

Wichtig: try/catch greift NUR, wenn await davor steht. Wer lade() ohne await aufruft, bekommt ein rejected Promise, das im aktuellen Stack-Frame nicht abgefangen wird — sondern als unhandledrejection landet.

Was try/catch NICHT abfängt

Ein paar Fehler-Quellen liegen außerhalb des sync try/catch:

  • Asynchrone Callbacks (in setTimeout, Event-Listenern, Promise-then ohne await): der Fehler entsteht in einem neuen Stack-Frame, der vom umgebenden try nichts weiß.
  • Promise-Rejections ohne await: gehen direkt an den unhandledrejection-Handler.
  • Syntax-Fehler beim Parsen: passieren vor jeder Ausführung, kein Code läuft.
JavaScript nicht-abgefangen.js
try {
    setTimeout(() => {
        throw new Error('zu spät');   // nicht abgefangen!
    }, 0);
} catch (e) {
    console.log('Hier nicht:', e.message);
}

// Statt dessen im Callback selbst try/catch
setTimeout(() => {
    try {
        throw new Error('jetzt schon');
    } catch (e) {
        console.log('Im Callback:', e.message);
    }
}, 0);
Output
Im Callback: jetzt schon

(Das erste Beispiel triggert ggf. einen globalen Uncaught-Handler je nach Umgebung — Node, Browser, Test-Runner verhalten sich unterschiedlich.)

Interessantes

catch fängt JEDEN Wert, nicht nur Error

throw 'foo' ist syntaktisch erlaubt — und das catch bekommt einen String. Konvention ist trotzdem, Error-Instanzen zu werfen, weil sie Stack-Trace, .message, .name und ggf. .cause tragen. Linter wie ESLint warnen mit no-throw-literal bei throw 'foo'.

Optional Catch Binding seit ES2019 — catch {} ohne Parameter

Vorher war catch (err) immer Pflicht, auch wenn err nie benutzt wurde. Seit ES2019 reicht catch { ... } ohne Parameter. Praktisch in Code, der nur „funktioniert oder nicht?" prüft. Babel/TypeScript transpilieren das für ältere Targets nahtlos.

finally überschreibt Control-Flow — Anti-Pattern

return, break, continue oder throw in einem finally-Block überschreiben das, was aus dem try kam — auch eine ausstehende Exception. ESLint warnt mit no-unsafe-finally. Saubere Regel: finally nur für Cleanup-Code, keine Control-Flow-Statements darin.

Error.cause seit ES2022 — sauber typisierter Re-throw

throw new Error('Wrapper', { cause: original }) ist seit ES2022 spezifiziert. Vorher haben Codebasen das ad-hoc als eigene Properties gelöst (err.original = ...) — uneinheitlich. Mit cause ist es jetzt Standard, und Debugger/Stack-Traces zeigen die Kette an.

Mehrere catch-Klauseln (wie in Java) gibt es nicht

JavaScript kennt nur ein catch pro try. Typ-Filterung passiert intern per instanceof. Das Pattern-Matching-Proposal (Stage 1) hätte langfristig potenziell Sub-Pattern in catch — aber das ist Zukunfts-Musik, derzeit kein konkreter Spec-Vorschlag.

try/catch deaktivierte historisch V8-Optimierungen — heute nicht mehr

Bis V8 5.x (2016) wurde Code in try-Blöcken vom Crankshaft-Optimierer ignoriert — performance-kritische Hot-Loops mit try/catch waren spürbar langsamer. Mit dem neuen Turbofan-Compiler ist das Geschichte; try/catch ist heute kein Performance-Argument mehr.

Try-Block mit nur finally — kein catch erlaubt? Doch

try { ... } finally { ... } ohne catch ist gültig. Fehler im try-Block propagieren dann nach oben, aber das finally läuft noch davor. Klassisches Muster für Cleanup-without-handling — wenn der Aufrufer die Fehler behandeln soll, aber Ressourcen trotzdem freigegeben werden müssen.

Async-await + try/catch ist erst seit ES2017 idiomatisch

Vorher war .catch() auf der Promise-Kette die einzige Form. Seit async/await ist try/catch um await der lesbarere Standard. Die beiden Formen sind funktional äquivalent — manche Style-Guides bevorzugen die Promise-Variante für reine Promise-Ketten ohne Zwischen-Logik.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Kontrollstrukturen

Zur Übersicht