Almost everything useful in a server takes time: reading a file, asking a database, calling another service. If the program stopped and waited for each one, a single slow request would freeze every other visitor. Node.js avoids this with asynchronous code. This lesson is the most important idea in the whole course, so take your time.
One thread and an event loop
Your JavaScript runs on a single thread, which means one line at a time. But slow work, such as file and network operations, is handed to the operating system. While the system works, Node carries on with other code. When the work is done, the result is put in a queue, and the event loop takes it from the queue and runs your code that was waiting for it. Think of a waiter in a restaurant. The waiter does not stand at the kitchen door until your meal is ready. They take your order, serve other tables, and bring your food when the kitchen calls.
This design handles thousands of waiting connections with little memory. The price is one rule: never block the thread with long work. If you run a loop that takes five seconds, no other request is answered for five seconds.
Promises
A promise is an object that stands for a value that is not ready yet. It is in one of three states: pending, fulfilled (with a value) or rejected (with an error). You attach code to it with .then for success and .catch for failure. Most modern Node functions, including fs/promises and fetch, return promises.
async and await
async and await let you write promise code that reads from top to bottom. Putting await before a promise pauses that function until the promise settles, and gives you the value. The program as a whole is not paused: other work continues. You can use await only inside an async function, or at the top level of an ES module file. Run this example.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
console.log('1 start');
setTimeout(() => console.log('3 timer'), 0);
Promise.resolve().then(() => console.log('2.5 promise'));
console.log('2 end of script');
async function getUser(id) {
await sleep(50);
if (id !== 1) throw new Error('No user ' + id);
return { id, name: 'Asha' };
}
try {
console.log(await getUser(1));
console.log(await getUser(2));
} catch (e) { console.log('caught:', e.message); }
const t = Date.now();
const [a, b] = await Promise.all([getUser(1), getUser(1)]);
console.log('parallel ok', Date.now() - t < 120, a.name, b.name);
node async.js
1 start
2 end of script
2.5 promise
3 timer
{ id: 1, name: 'Asha' }
caught: No user 2
parallel ok true Asha Asha
Study the order of the first four lines. The code runs from top to bottom: line 1, then it schedules a timer and a promise, then line 2. Only after the main script finishes does Node run the waiting work. Promise callbacks run before timer callbacks, even a timer set to zero milliseconds. This surprises many beginners, and you do not need to memorise it. Just remember that async results never appear in the middle of your current code.
Handling errors
When an awaited promise is rejected, await throws the error, and you catch it with an ordinary try and catch, as shown with getUser(2). If you forget, an unhandled rejection can crash your program or leave a request hanging. Two habits help: use try and catch where you can do something useful about the error, and let other errors travel upwards to one central place. In Express, that central place is an error handler, which you will build in lesson 10.
Sequential or parallel?
Two awaits one after the other run one after the other. If the two tasks do not depend on each other, start both first and wait for both together with Promise.all. In the example, two 50 ms calls finish in about 50 ms in total, not 100 ms. If one promise in Promise.all rejects, the whole thing rejects. Use Promise.allSettled when you want every result, whether it worked or failed.
// slower: one after the other
const a = await getUser(1);
const b = await getUser(2);
// faster when independent: both at once
const [x, y] = await Promise.all([getUser(1), getUser(1)]);
A common mistake: forgetting await
If you forget await, you get a promise object instead of the value. You will see output such as Promise { <pending> }, and a later line fails with an odd error. When a value looks strange, check that every async call has await. Another mistake is using await inside forEach. The loop does not wait. Use a for...of loop, or Promise.all with map.
Do not block the event loop
- Avoid long synchronous work in request code, such as huge loops or reading a big file with
readFileSync. - Functions ending in
Syncblock the thread. They are fine in a start-up script, but not while serving requests. - For truly heavy computing, use worker threads or a separate service.
Your task
- Create
async.jsand run it. Predict the order of the first four lines before you run it. - Add
getUser(3)andgetUser(4)insidePromise.alland measure the time withDate.now(). - Change
Promise.alltoPromise.allSettledand print the results. Make one of the calls fail on purpose. - Remove one
awaiton purpose and see what is printed. Then put it back.
How to check it worked
- The first four lines print in the order
1,2,2.5,3. - The parallel version takes about 50 ms, not 100 ms, and prints
parallel ok true. - You can say what
awaitdoes in your own words.