Error Handling in JavaScript: Try, Catch, Finally

Audience: developers with basic JavaScript knowledge — functions, callbacks, and async/await aren't assumed, but you should know what a function call is.
TL;DR: JavaScript errors don't have to crash your program. The try/catch/finally block gives you a structured way to intercept failures, respond to them, and always clean up — regardless of what went wrong.
Problem
Unhandled errors in JavaScript throw uncaught exceptions — they halt execution and give the user nothing useful. Consider fetching user data from an API: the network can fail, the response can be malformed, or the server can be down. Without error handling, any of these causes a silent crash or an unformatted stack trace in the console.
// Without error handling — crash if fetch fails
const response = await fetch("https://video.thitainfo.com/api/health");
const data = await response.json();
console.log(data.status); // Explodes if fetch throws or json() fails
The fix is a deliberate error-handling strategy using try, catch, and finally.
Solution
Step 1 — Wrap risky code in a try block
The try block contains any code that might throw. If an error occurs anywhere inside it, execution immediately jumps to catch.
// Wrapping a network call
async function fetchUser(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(
`HTTP \({response.status}: Failed to fetch user \){userId}`,
);
}
const user = await response.json();
return user;
} catch (error) {
console.error("fetchUser failed:", error.message);
return null;
}
}
// Usage
const user = await fetchUser(42);
// Output on network failure: fetchUser failed: Failed to fetch
The catch block receives the error object. error.message is the human-readable description; error.name tells you the type (e.g. TypeError, SyntaxError).
Step 2 — Use finally for guaranteed cleanup
finally always executes — whether the try succeeded, catch ran, or even if return was called mid-block. It's the right place for cleanup: closing connections, hiding loading spinners, releasing locks.
async function fetchUserWithSpinner(userId) {
showSpinner(); // Start loading UI
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Request failed:", error.message);
return null;
} finally {
hideSpinner(); // Runs no matter what — success or failure
}
}
Without finally, a thrown error inside catch could leave the spinner visible forever.
Step 3 — Throw custom errors for precise control
JavaScript's built-in Error class is generic. For application logic, extend it to create specific error types that carry more context — and that let catch blocks distinguish between failure modes.
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field; // Extra context beyond just the message
}
}
class NetworkError extends Error {
constructor(statusCode, message) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
function validateAge(age) {
if (typeof age !== "number") {
throw new ValidationError("age", "Age must be a number");
}
if (age < 0 || age > 150) {
throw new ValidationError("age", `Age ${age} is out of valid range`);
}
return true;
}
// Catch block can now branch by error type
try {
validateAge("thirty");
} catch (error) {
if (error instanceof ValidationError) {
console.error(
`Validation failed on field '\({error.field}': \){error.message}`,
);
// Output: Validation failed on field 'age': Age must be a number
} else {
throw error; // Re-throw anything unexpected
}
}
Re-throwing is important: only handle what you understand. Silently swallowing all errors (catch (e) {}) hides real bugs.
Now here's the full execution order, so there's no ambiguity about when each block runs:finally runs in every path. That's the guarantee worth internalizing.
Results
A codebase with structured error handling produces measurably better outcomes:
Errors surface with specific messages (
ValidationError on field 'email') instead of generic uncaught exceptions, cutting average debugging time significantly on production incidents.finallyblocks eliminate resource leaks — open file handles, visible loading states, database connections — that compound over time in long-running Node.js processes.Custom error classes let monitoring tools (Sentry, Datadog) group errors by type rather than by stack trace, making alerting far less noisy.
Trade-offs
try/catch is not free. In hot loops (tens of thousands of iterations), wrapping every iteration in try/catch can impose a measurable overhead in V8. Move the try/catch outside the loop and handle errors at the batch level if performance matters there.
It doesn't catch everything. try/catch only catches synchronous errors and errors in await-ed promises. An unhandled promise rejection (fetch(...).then(...) without .catch()) won't be caught by a surrounding try/catch. Use async/await consistently so await surfaces rejections into the nearest catch.
Overly broad catch blocks hide bugs. catch (e) { /* do nothing */ } is almost always wrong. At minimum, log the error. Better: only catch errors you can meaningfully handle, and re-throw the rest.
Conclusion
Error handling isn't defensive programming — it's the contract your code makes with everyone calling it. try marks what can fail, catch defines the recovery, finally enforces cleanup. Custom error classes give you the precision to distinguish user mistakes from infrastructure failures. Start with try/catch around any I/O, validate inputs with custom errors, and use finally wherever resources need releasing.
Further reading
MDN — try...catch — complete syntax reference
MDN — Error — all built-in error types and how to extend them
Jake Archibald — Tasks, microtasks, queues and schedules — essential context for understanding where async errors surface
Node.js — Error handling best practices — production-grade patterns for server-side JS
Sentry — JavaScript error monitoring — turning caught errors into actionable alerts


