Error Handling
A program can fail at runtime, and error handling is how we deal with that. We have not covered it in the earlier chapters, so we start here with the basics (try/catch/finally and the throw statement), and then we look at how error handling works with callbacks, Promises, and async/await.
try/catch/finally
The try block contains code that might throw. The catch block runs if an error is thrown. The finally block always runs, regardless of whether an error occurred.
try {
const result = riskyOperation();
console.log(result);
} catch (error) {
console.error("Something went wrong:", error.message);
} finally {
console.log("Cleanup complete");
}
catchreceives the error object and can inspect itsmessageandstackpropertiesfinallyis useful for cleanup: closing connections, clearing timers, resetting state
Throwing Errors with throw
Use throw to signal an error condition. Always throw Error objects (not strings or numbers) so you get a stack trace:
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero");
}
return a / b;
}
try {
divide(10, 0);
} catch (error) {
console.error(error.message); // "Division by zero"
}
// Always use Error objects
throw new Error("Something went wrong"); // Good
throw "Something went wrong"; // Avoid — no stack trace
Built-in Error Types and When They Occur
| Type | Typical cause |
|---|---|
Error |
Generic error |
TypeError |
Wrong type (e.g., calling a non-function) |
ReferenceError |
Undefined variable |
SyntaxError |
Invalid syntax |
RangeError |
Value out of range |
throw new TypeError("Expected a string, got number");
throw new RangeError("Index must be between 0 and 100");
Error Handling in Async Code
Callbacks: Error-First Convention
As covered in the Callbacks section, Node.js callbacks pass the error as the first argument:
import fs from "node:fs";
fs.readFile("config.json", "utf8", (error, data) => {
if (error) {
console.error("Read failed:", error.message);
return;
}
console.log(data);
});
Promises: .catch()
A .catch() at the end of a chain handles rejections from any preceding .then():
fetchUser(1)
.then((user) => fetchPosts(user.id))
.then((posts) => console.log(posts))
.catch((error) => {
console.error("Error:", error.message);
});
You can also recover from errors mid-chain by returning a fallback value from .catch():
fetchUser(1)
.then((user) => fetchAvatar(user.id))
.catch((error) => {
console.error("Avatar failed, using default");
return "/images/default-avatar.png"; // Recovery value
})
.then((avatarUrl) => {
displayAvatar(avatarUrl); // Continues with the fallback
});
Async/Await: try/catch
With async/await, use the same try/catch you would use for synchronous code:
async function loadUserData(id) {
try {
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (error) {
console.error("Failed to load:", error.message);
throw error;
}
}
Handling Partial Failures
When running multiple async operations where some may fail, Promise.allSettled lets you inspect each result individually instead of failing fast. See the Promise Combinators section for full examples and comparison with Promise.all.
Best Practices
-
Always use Error objects: They carry a stack trace, which makes debugging easier than it is with a thrown string or number
-
Do not discard errors silently: An empty
catchblock hides bugs and makes failures harder to diagnose. At minimum, log the error. If you cannot handle it, re-throw it:catch (error) { if (error instanceof ValidationError) { showFieldError(error.field, error.message); } else { throw error; // Let unexpected errors propagate } } -
Use
finallyfor cleanup: Ensure resources are released regardless of success or failure:const connection = await openDatabase(); try { await connection.query("..."); } finally { await connection.close(); // Always runs } -
Always handle Promise rejections: In modern Node.js, unhandled rejections can terminate the process. In browsers, they trigger
unhandledrejectionevents and console errors. Attach.catch()to Promise chains, or usetry/catchwithawait. -
Create custom error types for complex applications:
class ValidationError extends Error { constructor(field, message) { super(message); this.name = "ValidationError"; this.field = field; } } throw new ValidationError("email", "Invalid email format");