The problem: slow operations
Almost everything interesting in a backend is asynchronous: reading a file, talking to a database, calling another API. Node has had three styles to handle it, and it is worth knowing all three.
1. Callbacks and the (err, data) pattern
Node's original style: you pass a continuation function (callback)
that Node calls when it finishes. By convention, the first argument is the
error (or null if everything went fine) and the second, the data:
const fs = require("fs");
fs.readFile("data.txt", "utf8", (err, data) => {
if (err) {
console.error("Failed:", err.message);
return; // always check err first!
}
console.log(data);
});
The problem: nesting several callbacks leads to "callback hell", pyramid-shaped code that is hard to read and to handle errors in.
2. Promises
A promise represents a value that will be available in the future. It is
chained with .then() (success) and .catch() (error), in a flat way:
const fs = require("fs/promises"); // promise-based version of fs
fs.readFile("data.txt", "utf8")
.then((data) => console.log(data))
.catch((err) => console.error(err.message));
3. async / await
It is syntactic sugar over promises: you write asynchronous code that reads
like synchronous code. An async function always returns a promise, and await
pauses until the promise resolves. Errors are caught with a
normal try / catch:
import { readFile } from "fs/promises";
async function read(path) {
try {
const data = await readFile(path, "utf8");
return data;
} catch (err) {
console.error("Could not read:", err.message);
throw err; // or return a default value
}
}
In this module's exercise you will write
asyncfunctions that receive a data source and await it withawait. This way you practice the asynchronous flow without depending on real files (which do not exist in this environment).
Examples
An async function returns a promise; you await it with await
async function double(n) {
return n * 2; // even without await, it returns a promise
}
const r = await double(21);
console.log(r); // 42