Some things take time: reading a file, calling a database, asking another server for data. JavaScript has one main thread, so if it waited for each slow task, everything else, including the page or the server, would freeze. Instead it starts the task, carries on, and comes back when the result is ready. This is asynchronous programming, and every MERN app is full of it: each database query, each API call and each file upload is asynchronous.
Callbacks: the first idea
A callback is a function you give to something else to call later. setTimeout(() => console.log('done'), 100) means: run this function in 100 milliseconds. Callbacks work, but when one slow task depends on the result of another, you end up nesting them deeper and deeper, which is hard to read and hard to handle errors in.
Promises: a value that arrives later
A promise is an object that stands for a result that is not ready yet. It is in one of three states: pending, fulfilled (it has a value), or rejected (it has an error). You attach handlers with .then() for success and .catch() for errors, and you can chain them. Most libraries you will use, including fetch and Mongoose, return promises.
async and await: promises that read like normal code
Putting async in front of a function lets you use await inside it. await pauses that function (not the whole program) until the promise settles, then gives you the value. Errors become normal exceptions, so you catch them with try / catch. Compare the code in the file below: it does the same thing three ways.
// Lesson 4: asynchronous JavaScript. Run: node examples/04-async/async-tour.js
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// 1) A callback: "call me when you are done" (the old style)
setTimeout(() => console.log('callback: 100 ms passed'), 100);
// 2) A promise: a value that arrives later
const loadJob = (id) =>
new Promise((resolve, reject) => {
setTimeout(() => (id > 0 ? resolve({ id, role: 'Teller' }) : reject(new Error('Bad id'))), 50);
});
loadJob(1).then((job) => console.log('promise:', job.role)).catch((err) => console.log(err.message));
// 3) async / await: the same thing, written like normal top-to-bottom code
async function main() {
await wait(150);
try {
const job = await loadJob(2);
console.log('await:', job.role);
await loadJob(0); // this one rejects
} catch (err) {
console.log('caught:', err.message); // errors from await are caught with try/catch
}
// run things at the same time, wait for all of them
const [a, b] = await Promise.all([loadJob(3), loadJob(4)]);
console.log('Promise.all:', a.id, b.id);
// fetch is built into Node 18+ and into every browser
const server = (await import('node:http')).createServer((req, res) => res.end(JSON.stringify({ ok: true }))).listen(0);
const res = await fetch(`http://localhost:${server.address().port}/`);
console.log('fetch:', res.status, await res.json());
server.close();
}
main();
Run it and read the output order carefully. The promise result prints before the callback, because the promise takes 50 ms and the callback 100 ms. Understanding that things finish in the order of their delays, not in the order you wrote them, is the key idea of this lesson.
Run tasks at the same time
If two things do not depend on each other, do not await them one after the other. await Promise.all([a(), b()]) starts both at once and waits for both, so the total time is the slower of the two instead of the sum. You will use this pattern in lesson 18 to fetch a page of jobs and the total count in one go.
fetch: calling an API
fetch(url) is built into every modern browser and into Node.js 18 and newer. It returns a promise of a Response. Two details trip up almost every beginner. First, fetch only rejects when the network fails; a 404 or 500 answer is still a successful fetch, so you must check response.ok yourself. Second, reading the body is also asynchronous: await response.json(). The tour file shows the whole sequence.
Your task
- Run the tour and note the order of the printed lines.
- Change the delay in the callback to 10 ms and run it again. Which line moves, and why?
- Write an
asyncfunctionloadTwo()that usesPromise.allto load two jobs, and time it withconsole.time. Then do it again with two separate awaits and compare the times. - Make
fetchfail on purpose by calling a port nothing listens on, and catch the error withtry / catch.
How to check it worked
- You can predict the order of the output before you run the file.
- Your parallel version takes about as long as one request, not two.
Summary
- Slow tasks do not block JavaScript. They finish later and hand back a result.
- Promises represent that result;
asyncandawaitmake them readable. try / catchhandles errors;Promise.allruns independent tasks together.fetchdoes not reject on 404 or 500; checkresponse.ok.