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.
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{ ok: true }
Parse-Fehler: Unexpected token 'd', "das ist no"... is not valid JSON
nullDer 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).
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);
}Fehler: Division durch Null
Typ: ErrorJavaScript 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) {}.
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')); // falsetrue
falsePraktisch 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.
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());Open
Use 1
Close 1
Ergebnis: OKfinally 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".
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 propagiertaus finally
unterdrücktDas 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".
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);
}LOG: Unexpected token 'd', "das ist kaputt" is not valid JSON
Aufrufer: Unexpected token 'd', "das ist kaputt" is not valid JSONMit Error.cause (ES2022) lässt sich der ursprüngliche Fehler an einen neuen anhängen — sauber typisierter Re-throw mit Zusatz-Information:
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);
}Fehler: Konfig konnte nicht geladen werden
Ursprung: Unexpected token 'd', "das ist kaputt" is not valid JSONVerschachtelte 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.
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);
}Innen: A
Aussen: B aus inner-catchSinnvoll, 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.
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
}
}Typ-Fehler: Number erwartetWichtige 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.
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();Async-Fehler: Server tot
AufräumenWichtig: 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-thenohneawait): der Fehler entsteht in einem neuen Stack-Frame, der vom umgebenden try nichts weiß. - Promise-Rejections ohne
await: gehen direkt an denunhandledrejection-Handler. - Syntax-Fehler beim Parsen: passieren vor jeder Ausführung, kein Code läuft.
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);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
- try...catch – MDN
- throw – MDN
- Error – MDN
- The try Statement – ECMAScript Spec
- no-unsafe-finally – ESLint