Async and Promises
Reading a file, querying a database, and calling another server all take time. If Node.js stood still during each one, the whole program would freeze. Instead, Node.js is built around asynchronous code: it starts a slow job, keeps doing other work, and comes back when the job finishes. This design is why one small server can serve thousands of visitors.
A promise is an object that represents work that has not finished yet. Think of a restaurant receipt: the kitchen is cooking, and the receipt tells you your order is coming. A promise can succeed, which is called resolving, or fail, which is called rejecting. The then method runs on success, and catch runs on failure.
The modern way to use promises is async and await. You mark a function with async, and inside it you can await a promise. Await pauses that one function until the promise settles, without blocking the rest of the program. The code reads top to bottom like ordinary JavaScript, which is far easier to follow than nested callbacks.
Nested callbacks have a well-known problem called callback hell: every step indents deeper, and the code becomes hard to read and fix. Promises flatten the same logic, and async await flattens it further. In the real world you will write async functions that await database queries, API calls, and file reads, so this style is worth getting comfortable with early.
A promise in action
The fs module has promise versions of its methods. fs.promises.readFile returns a promise instead of using a callback. The then method runs when the file is read. The catch method runs when something goes wrong, like when the file does not exist. The two handlers make the success and failure paths visible side by side.
const fs = require("fs");
fs.promises
.readFile("message.txt", "utf8")
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log("Read failed:", err.message);
});
Same job with async await
The await keyword makes the same code look like normal top-to-bottom JavaScript. The try and catch blocks handle errors, with try wrapping the work that might fail. This version is shorter and easier to debug, which is why async await is the style you will see in most modern node code.
const fs = require("fs");
async function readMessage() {
try {
const data = await fs.promises.readFile("message.txt", "utf8");
console.log(data);
} catch (err) {
console.log("Read failed:", err.message);
}
}
readMessage();
Blocking vs non-blocking, in plain words
The event loop is the scheduler behind all of this. Node.js has one main thread, so it cannot literally do two things at once. What it does is hand slow jobs to the operating system and remember the callbacks. When a job finishes, the event loop runs the waiting callback. A sync file read skips the loop entirely and makes the program wait, which freezes every visitor on a server.
// This blocks the server: no one gets answered while the disk works
const data = fs.readFileSync("message.txt", "utf8");
// This does not: the server keeps running until the read finishes
const data = await fs.promises.readFile("message.txt", "utf8");
Syntax
const fs = require("fs");
async function readMessage() {
try {
// await pauses this function only, not the whole program
const data = await fs.promises.readFile("message.txt", "utf8");
console.log(data);
} catch (err) {
console.log("Read failed:", err.message); // runs if the read rejects
}
}
readMessage(); // start the async work
Callback Hell Is Avoidable with Promises
Nest 5 callbacks deep becomes unreadable — use async/await with fs.promises.
const data = await fs.promises.readFile("f.txt","utf8")
Good to know
- Promises represent work that is still running.
- then runs on success, catch runs on failure.
- await pauses one function, not the whole program.
- Sync file calls block the event loop and freeze the server.