Handling Errors in Async Code
This chapter builds on the previous chapter, "Working Asynchronously." We assume you know what callbacks, promises, and async/await are, and have Node.js installed.
Things go wrong. Files are missing, servers are down, numbers arrive where words were expected. In normal (synchronous) code, an error stops the program right there, and a try/catch block can catch it. Asynchronous code is trickier: the error often happens later, after your function has already finished. So each async style has its own way of reporting and handling errors. This chapter shows you all three.
Errors with Callbacks
A callback is a function you hand to another function, to be called when the work is done. The Node.js rule is simple: the first argument of the callback is the error, or null if all went well.
const { readFile } = require('node:fs');
readFile('notes.txt', 'utf8', (err, data) => {
if (err) {
console.error('Something went wrong:', err.message);
return;
}
console.log(data);
});Always check err first and return early. If you forget, your program may crash or, worse, use data when it does not exist.
Errors with Promises
A promise is an object that represents work still in progress. It can end in two ways: fulfilled (success) or rejected (failure). You handle rejection with .catch():
const { readFile } = require('node:fs/promises');
readFile('notes.txt', 'utf8')
.then((data) => console.log(data))
.catch((err) => console.error('Could not read file:', err.message));The nice part: one .catch() at the end catches errors from any step in the chain above it.
Errors with async/await
await pauses inside an async function until a promise settles. If it rejects, the rejection becomes a thrown error — so the familiar try/catch works again:
const { readFile } = require('node:fs/promises');
async function main() {
try {
const text = await readFile('notes.txt', 'utf8');
console.log(text);
} catch (err) {
console.error('Could not read file:', err.message);
}
}
main();Callout: the forgotten await
Choosing a Style
| Style | How errors arrive | Best for |
|---|---|---|
| Callback | First argument, `err` | Old APIs, event-style code |
| Promise | `.catch()` on the chain | Chaining steps, libraries |
| async/await | Regular `try`/`catch` | Most modern code |
For new code, prefer async/await: it reads top-to-bottom and uses the error handling you already know.
Diagram
Show diagram source
flowchart TD
A[File read fails] --> B[Error object created]
B --> C[Callback style: err first argument]
B --> D[Promise style: rejection to .catch]
B --> E[async/await: throw inside try, caught by catch]Exercise
Exercise
You call an async function that reads a file, but you forget the try/catch. What happens when the file is missing?
Show answer
The promise rejects and nothing handles it. Node reports an "unhandled promise rejection" and, in recent versions, crashes the program. Wrap the await in try/catch, or attach .catch().
Exercise
In a callback-style function, why must you check the error argument first and return?
Show answer
Because if an error occurred, the success value (like the file contents) is not there. Continuing would use undefined data or crash. Checking first and returning keeps the failure path clean.
Spotted a mistake, or code that does not work?