🧯 Error Handling in Node, Python & PHP
Software fails: files go missing, networks drop, users type nonsense. What separates a fragile script from a robust backend is how gracefully it handles the failures it can't prevent. This lesson compares the language-level error tools of Node.js, Python, and PHP — the try blocks, the raise/throw keywords, custom error classes, and the cleanup step that always runs.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Write try/catch (JS), try/except/else/finally (Python), and try/catch/finally (PHP) blocks correctly
- Throw / raise errors and define your own custom error & exception classes
- Explain how errors propagate up the call stack until something catches them
- Use finally (and else) for reliable cleanup
- Apply best practices: catch narrowly, never swallow silently, fail loudly at the right layer
Estimated Time: 30–40 minutes • Difficulty: Intermediate
Hands-on: Harden a "parse a config value" function in all three languages with a custom error and a guaranteed-cleanup step.
In This Lesson
Why Structured Error Handling?
An error (or exception) is a signal that a piece of code could not do its job. Rather than letting the program limp on with bad data, the language stops the current path and hands control to whoever is prepared to deal with the problem. Structured handling gives you three things a scattering of if checks cannot: a clear place to react, a way to separate the happy path from the failure path, and a guarantee that cleanup still runs.
💡 A useful analogy: Think ofthrow/raiseas pulling the emergency cord on a train. Everything after it in the current carriage stops immediately, and the train keeps rolling backward through the carriages (the call stack) until it reaches a station (acatch/except) that's staffed to handle the emergency. If no station is staffed, the whole train stops — an unhandled crash.
All three languages share the same core idea — a protected block plus a handler — but they differ in vocabulary and in a few important details, which we'll line up next.
📖 Key Terms
Throw / raise: to signal an error, interrupting normal flow.
Catch / except: to intercept a thrown error and decide what to do.
Propagation: an uncaught error travelling up the call stack to the caller.
Finally: a block that runs whether or not an error occurred — for cleanup.
The try Block in Three Languages
Here is the same defensive pattern — attempt something risky, react on failure, always clean up — expressed in each language.
JavaScript (Node.js) — try / catch / finally
try {
const data = JSON.parse(rawInput); // may throw a SyntaxError
console.log(data.name);
} catch (err) {
// err is the thrown Error object
console.error('Could not parse input:', err.message);
} finally {
console.log('Done attempting.'); // always runs
}
JavaScript has a single catch clause. To react differently to different error types, inspect the caught object inside the block — usually with instanceof:
catch (err) {
if (err instanceof SyntaxError) {
// handle malformed JSON
} else {
throw err; // re-throw anything we didn't expect
}
}
Python — try / except / else / finally
try:
data = json.loads(raw_input) # may raise JSONDecodeError
except json.JSONDecodeError as err:
print(f'Could not parse input: {err}')
except (KeyError, ValueError) as err: # catch several types at once
print(f'Bad data: {err}')
else:
print(data['name']) # runs ONLY if no exception was raised
finally:
print('Done attempting.') # always runs
Python is the richest here: you can list multiple typed except clauses, and it adds an else block that runs only when the try succeeded — a clean way to keep the success-only code out of the protected block.
PHP — try / catch / finally with Throwable
<?php
try {
$data = json_decode($rawInput, true, 512, JSON_THROW_ON_ERROR);
echo $data['name'];
} catch (JsonException $err) { // typed catch, like Python
echo 'Could not parse input: ' . $err->getMessage();
} catch (Throwable $err) { // catch-all safety net
echo 'Unexpected: ' . $err->getMessage();
} finally {
echo 'Done attempting.'; // always runs
}
Like Python, PHP supports multiple typed catch clauses. The base type to catch is Throwable, which sits above both Exception and Error.
| Feature | JavaScript | Python | PHP |
|---|---|---|---|
| Protected block | try | try | try |
| Handler | catch (one) | except (many, typed) | catch (many, typed) |
| Success-only block | — | else | — |
| Always-runs block | finally | finally | finally |
| Base type | Error | Exception / BaseException | Throwable |
⚠️ Async errors in Node
A plain try/catch only catches errors thrown synchronously. To catch an error from a promise you must await it inside the try (or attach .catch()). A rejected promise that is never awaited or caught becomes an unhandled rejection. This lesson stays language-level; the async specifics are covered in Module 11.
Throwing & Raising
When your own code detects a bad situation, signal it explicitly rather than returning a magic value like -1 or null that the caller might forget to check.
// JavaScript — throw an Error instance
function withdraw(balance, amount) {
if (amount > balance) {
throw new Error('Insufficient funds');
}
return balance - amount;
}
# Python — raise an exception instance
def withdraw(balance, amount):
if amount > balance:
raise ValueError('Insufficient funds')
return balance - amount
<?php
// PHP — throw an exception instance
function withdraw(float $balance, float $amount): float {
if ($amount > $balance) {
throw new InvalidArgumentException('Insufficient funds');
}
return $balance - $amount;
}
⚠️ Always throw an object, not a string
JavaScript technically lets you throw 'oops', but don't — a plain string has no .message, no .stack, and breaks instanceof checks. Always throw an Error (JS), an Exception subclass (Python), or a Throwable (PHP) so handlers get a message and a stack trace.
Custom Error Classes
Built-in error types are fine, but a named error class makes your intent explicit and lets callers catch exactly the failure they care about. In every language you make one by extending the base error type.
// JavaScript — extend Error
class InsufficientFundsError extends Error {
constructor(deficit) {
super(`Short by ${deficit}`);
this.name = 'InsufficientFundsError'; // so stack traces read well
this.deficit = deficit; // attach useful context
}
}
// caller can now target it precisely:
try {
withdraw(50, 80);
} catch (err) {
if (err instanceof InsufficientFundsError) {
console.log('Top up by', err.deficit);
}
}
# Python — subclass Exception
class InsufficientFundsError(Exception):
def __init__(self, deficit):
super().__init__(f'Short by {deficit}')
self.deficit = deficit
try:
withdraw(50, 80)
except InsufficientFundsError as err:
print('Top up by', err.deficit)
<?php
// PHP — extend Exception
class InsufficientFundsException extends Exception {
public function __construct(
public readonly float $deficit
) {
parent::__construct("Short by {$deficit}");
}
}
try {
withdraw(50, 80);
} catch (InsufficientFundsException $err) {
echo 'Top up by ' . $err->deficit;
}
✅ Why bother with custom classes?
They turn error handling into a clear contract. A caller can write catch (InsufficientFundsError) and know it will not accidentally swallow an unrelated TypeError. They also carry structured data (here, deficit) instead of forcing you to parse a message string.
Propagation & Cleanup
If a block doesn't catch an error, it doesn't vanish — it propagates up to the function that called it, then that function's caller, and so on up the stack until something catches it or the program crashes. This is powerful: low-level code can raise, and a single high-level handler can deal with it.
saveOrder throws; checkout ignores it; the top-level requestHandler catches it once, in one place.finally always runs
The finally block executes whether the try succeeded, threw, or even returned — making it the reliable place to release resources like file handles or database connections.
# Python — the file closes even if processing raises
f = open('data.txt')
try:
process(f.read())
finally:
f.close() # guaranteed
# Idiomatic Python prefers a context manager, which does this for you:
with open('data.txt') as f:
process(f.read()) # file auto-closes on exit, error or not
// JavaScript — release a lock no matter what
const release = await lock.acquire();
try {
await doWork();
} finally {
release(); // guaranteed
}
💡 else vs the end of try (Python)
Putting success-only code in an else block instead of at the bottom of try keeps the protected region as small as possible. That way a KeyError from your success code isn't accidentally caught by the except meant for the risky call.
Best Practices
The mechanics are similar across languages; the discipline is what makes error handling good. These principles hold in Node, Python, and PHP alike.
✅ Do
- Catch narrowly. Handle the specific type you expect (
JSONDecodeError,InsufficientFundsError) rather than a blanket catch-all. - Add context, then re-throw. If a layer can't fully handle an error, wrap it with more detail and re-throw so an upper layer can decide.
- Clean up in
finally(or a context manager /using-style helper). - Fail fast on programmer errors. A null where you required a value is a bug — let it crash loudly in development.
- Log with the stack trace, not just the message.
⚠️ Don't
- Swallow silently. An empty
catch {}/except: passhides bugs and produces the dreaded "nothing happened and no error." - Catch everything at the lowest level. Let errors propagate to a layer that can actually make a decision (retry, respond 500, show a message).
- Use exceptions for normal flow. "User not found" during a routine lookup is often a return value, not an exceptional event.
- Leak internals to users. Log the full trace server-side; show the user a friendly, generic message.
# Anti-pattern: the silent swallow — never do this
try:
risky()
except Exception:
pass # the error is gone forever; good luck debugging
# Better: catch what you expect, log the rest, re-raise the unknown
try:
risky()
except TimeoutError as err:
logger.warning('Retrying after timeout: %s', err)
retry()
Hands-on Exercise
🏋️ A Resilient Config Reader
Objective: Write a getPort(config) function that returns a valid port number, in all three languages. It must:
- Throw/raise a custom error (e.g.
ConfigError) if the"port"key is missing or not a number between 1 and 65535. - Use a finally step that logs
"config check complete"whether it succeeded or failed. - Be called inside a
trythat prints a friendly message on failure.
💡 Hint
Define the custom error by extending the base type (Error / Exception / Exception). Do your validation, and if it fails, throw/raise your custom error with a clear message. Put the log line in finally so it runs on both paths.
✅ Solution
// JavaScript
class ConfigError extends Error {
constructor(msg) { super(msg); this.name = 'ConfigError'; }
}
function getPort(config) {
try {
const port = config.port;
if (typeof port !== 'number' || port < 1 || port > 65535) {
throw new ConfigError(`Invalid port: ${port}`);
}
return port;
} finally {
console.log('config check complete');
}
}
try {
console.log(getPort({ port: 3000 })); // 3000
console.log(getPort({})); // throws
} catch (err) {
if (err instanceof ConfigError) console.error('Config problem:', err.message);
else throw err;
}
# Python
class ConfigError(Exception):
pass
def get_port(config):
try:
port = config.get('port')
if not isinstance(port, int) or not (1 <= port <= 65535):
raise ConfigError(f'Invalid port: {port}')
return port
finally:
print('config check complete')
try:
print(get_port({'port': 3000})) # 3000
print(get_port({})) # raises
except ConfigError as err:
print('Config problem:', err)
<?php
class ConfigError extends Exception {}
function getPort(array $config): int {
try {
$port = $config['port'] ?? null;
if (!is_int($port) || $port < 1 || $port > 65535) {
throw new ConfigError("Invalid port: " . var_export($port, true));
}
return $port;
} finally {
echo "config check complete\n";
}
}
try {
echo getPort(['port' => 3000]) . "\n"; // 3000
echo getPort([]) . "\n"; // throws
} catch (ConfigError $err) {
echo 'Config problem: ' . $err->getMessage() . "\n";
}
🎯 Quick Quiz
Question 1: Which block is guaranteed to run whether or not an error was thrown, making it the right place for cleanup?
Question 2: In PHP, what is the broadest type you can catch to intercept both Exception and Error?
Question 3: Why is an empty catch {} / except: pass considered an anti-pattern?
Summary & Quiz
🎉 Key Takeaways
- All three languages share the pattern: a protected block + a handler + a cleanup step.
- JS has a single
catch(branch byinstanceof); Python and PHP allow multiple typedexcept/catchclauses. - Python adds an
elseblock; PHP's catch-all base type isThrowable. - Always throw an object (Error / Exception / Throwable), and prefer custom classes for precise catching and structured context.
- Uncaught errors propagate up the stack;
finallyguarantees cleanup; never swallow errors silently.
📚 Further Reading
🚀 What's Next?
With resilient code in hand, the next lesson puts it to work reading and writing the outside world — working with files and JSON in Node, Python, and PHP, where error handling and cleanup really earn their keep.
🎉 Nice work!
Your code now fails gracefully instead of mysteriously. Let's give it some data to chew on.