Exception handling

A runtime error is the one that waits until your program is already running and then, faced with something impossible — a division by zero, a file that isn't there, a wildly-typed number — throws up its hands and crashes. The whole program stops dead, mid-sentence, and the user is left staring at a wall of red text (or worse, nothing at all).

But a crash is not inevitable. When something goes wrong at runtime the language doesn't just quit silently — it raises an exception: a special "something has gone wrong here!" signal that shoots up through your program looking for someone to deal with it. If nobody does, it reaches the top and the program dies. Exception handling is how you volunteer to catch that signal and respond gracefully — recover, or at least fail cleanly — instead of letting the program fall over.

This is the difference between an app that pops up "Sorry, that file couldn't be opened — try another?" and one that vanishes without warning. Real, robust programs are full of exception handling, because the world they run in is full of things that can go wrong: bad input, missing files, dropped network connections, disks that fill up. A professional program expects trouble and plans for it.

The idea: try and catch

The tool has two halves. You put the code that might go wrong inside a try block, and the code that says what to do if it does inside a catch block that follows it. Think of it as a safety net strung under a tightrope walker: the walker (your risky code) tries to cross; if they fall, the net (the catch) catches them instead of letting them hit the ground.

\underbrace{\texttt{try}\;\{\ \dots\ \}}_{\text{code that might fail}}\qquad \underbrace{\texttt{catch}\;(\texttt{err})\;\{\ \dots\ \}}_{\text{what to do if it does}}

Here is a program that reads a number the user "typed". If it isn't a number, converting it fails and an exception is raised. Without a net the program would crash; with one, it catches the exception and prints a friendly message instead. Press Run:

function parseAge(input: string): number { const n = Number(input); if (Number.isNaN(n)) { throw new Error("That's not a number: \"" + input + "\""); } return n; } try { const age = parseAge("twelve"); // oops — not a number! console.log("Next year you'll be", age + 1); } catch (err) { // We land here instead of crashing. `err` is the exception that was raised. console.log("Couldn't read your age:", (err as Error).message); console.log("Please type digits, like 12."); } console.log("...and the program carries on happily.");

Notice the last line still prints. That is the whole point: the exception was handled, so the program didn't crash — it recovered and continued. The moment something inside try throws, the rest of the try block is skipped and control jumps straight into catch, where err holds the exception that was raised (usually an Error object with a .message).

Throwing your own exceptions

You don't have to wait for the language to spot trouble — you can raise an exception yourself with throw new Error("…"). This is how you signal that your own rules have been broken: an argument that makes no sense, a value out of range, a state that should never happen. Throwing is like pulling a fire alarm — it stops what you're doing immediately and hands the problem to whoever is prepared to deal with it.

A classic example is a divide function that refuses to divide by zero. Rather than return a meaningless result, it throws — and the caller wraps the call in a try/catch:

function divide(a: number, b: number): number { if (b === 0) { throw new Error("Cannot divide " + a + " by zero!"); // raise the alarm } return a / b; } const pairs: [number, number][] = [[10, 2], [7, 0], [9, 3]]; for (const [a, b] of pairs) { try { console.log(a + " / " + b + " = " + divide(a, b)); } catch (err) { console.log("Skipped " + a + " / " + b + " — " + (err as Error).message); } }

The middle pair (7 / 0) throws, so its message is caught and printed, and the loop simply moves on to the next pair. One bad case no longer sinks the whole batch. Notice how throw and catch work as a pair across a distance: the alarm is pulled deep inside divide, but it's answered up in the loop. The exception travels up the chain of calls until it finds a catch ready for it.

A tempting shortcut is to signal failure with a special return value — -1, null, or "ERROR". The trouble is that nothing forces the caller to check it. They can quietly use the -1 as if it were a real answer, and now you have a silent logic error instead of an obvious crash. An exception is impossible to ignore by accident: if you don't catch it, it stops the program loudly and points at the line. It also carries a proper message explaining what went wrong, and it separates the "happy path" of your code from the error-handling, so the normal logic stays clean and readable.

finally — the code that always runs

There is a third, optional block: finally. Whatever happens — whether the try succeeded, whether it threw and was caught, even whether you returned out of the middle — the finally block runs. Always. It's the "no matter what, do this on the way out" block.

What's it for? Cleanup. When you open a file, a database connection or a network socket, you must close it again — and you must close it even if something failed while you were using it, otherwise you leak resources. finally is the guaranteed place to put that closing step. Run this and watch the order the lines print in for both a good and a bad value:

function process(value: number): void { console.log("Opening the file..."); // pretend we opened a resource try { if (value < 0) { throw new Error("negative values are not allowed"); } console.log("Processed value:", value * 2); } catch (err) { console.log("Problem:", (err as Error).message); } finally { console.log("Closing the file."); // ALWAYS runs — success or failure } } process(5); // succeeds console.log("---"); process(-3); // throws, is caught — but the file still gets closed

For the good value you see open → process → close. For the bad value you see open → problem → close. Either way the file is closed. That guarantee is exactly why finally exists: the cleanup can't be skipped, forgotten, or jumped over by an error.

Putting it together

A full handler can use all three blocks. Read this as a sentence: "try to do the risky thing; if it goes wrong, catch the problem and deal with it; and finally, whatever happened, tidy up afterwards."

try { // 1. code that might raise an exception } catch (err) { // 2. runs ONLY if something in the try block threw } finally { // 3. runs ALWAYS, error or not — cleanup goes here }

The most damaging habit in exception handling is silently swallowing exceptions — an empty catch that does nothing:

try { doSomethingRisky(); } catch (err) { // 😱 nothing here — the error vanishes without a trace }

This looks tidy, but it's a trap. The program no longer crashes, so you feel safe — yet the bug is still there, now completely hidden. You've turned a loud, informative runtime error into a silent logic error that could give wrong answers for months before anyone notices. Catching an exception is a promise that you'll handle it.

The rule: only catch what you can genuinely handle. If you know what to do about a bad file — ask for another — then catch it and do that. If you don't actually know how to recover, don't muffle it: at the very least log or report it (console.log the message), or let it travel up to code that can deal with it. And remember that finally runs whether or not an error occurred — so cleanup belongs there, never buried in an empty catch that pretends nothing happened.