- Explain the difference between synchronous and asynchronous code and how
setTimeoutworks - Create a promise and handle its result with
then,catchandfinally - Write readable asynchronous code with
async/awaitandtry...catch - Run several tasks in parallel with
Promise.all
In a café the waiter passes your order to the kitchen but doesn't stand at the kitchen door until the plov is cooked — he serves other tables. When the food is ready, he brings it. JavaScript works the same way: while it waits for slow things like a server response, a timer or a file, it doesn't freeze the page but keeps doing other work. This is called asynchronous programming.
Synchronous and asynchronous
The code we have written so far was synchronous: each statement waits for the previous one to finish. setTimeout(function, ms) instead “puts a function aside” to run after the given number of milliseconds, and the program moves on to the next line immediately. Look at the order of the results:
console.log('1. Order placed');
setTimeout(() => {
console.log('4. Plov is ready');
}, 1000);
setTimeout(() => {
console.log('3. Tea is ready');
}, 0);
console.log('2. Waiter serves another table');▸ Expected output
1. Order placed 2. Waiter serves another table 3. Tea is ready 4. Plov is ready
JavaScript does only one thing at a time. Callbacks that are ready wait in a queue, and the event loop takes them from the queue one by one when the current synchronous code has finished. That is why even a setTimeout with a 0 ms delay runs after all the synchronous code.
Callbacks and their problem
The oldest way to get a result when async work is done is to pass a callback: “call this function when you're ready.” If several tasks must happen one after another, the callbacks nest inside each other and the code grows to the right like a staircase. Developers call this callback hell.
function cook(dish, callback) {
setTimeout(() => callback(`${dish} is ready`), 300);
}
cook('Plov', (first) => {
console.log(first);
cook('Dolma', (second) => {
console.log(second);
cook('Pakhlava', (third) => {
console.log(third);
});
});
});▸ Expected output
Plov is ready Dolma is ready Pakhlava is ready
Promises
A promise is an object that stands for a result that will be ready in the future. It has three states: pending (waiting), fulfilled (completed successfully) and rejected (ended with an error). When creating a promise, we get two functions: resolve for success and reject for failure. We take the result with then and the error with catch; finally runs in any case.
function cook(dish, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!dish) {
reject(new Error('No dish ordered'));
} else {
resolve(`${dish} is ready`);
}
}, time);
});
}
cook('Plov', 300)
.then((message) => {
console.log(message);
return cook('Dolma', 200);
})
.then((message) => console.log(message))
.then(() => cook('', 100))
.catch((error) => console.log('Error:', error.message))
.finally(() => console.log('Kitchen closed'));▸ Expected output
Plov is ready Dolma is ready Error: No dish ordered Kitchen closed
then returns the next step, and all errors are caught in one catch.async and await
async/await lets you write promise-based code as if it were ordinary synchronous code. A function declared with async always returns a promise. Inside it, await waits for a promise's result without freezing the rest of the program. Errors are caught with the familiar try...catch.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function cook(dish, time) {
await wait(time);
if (!dish) throw new Error('No dish ordered');
return `${dish} is ready`;
}
async function dinner() {
try {
console.log(await cook('Plov', 300));
console.log(await cook('Dolma', 200));
console.log(await cook('', 100));
} catch (error) {
console.log('Error:', error.message);
} finally {
console.log('Kitchen closed');
}
}
dinner();▸ Expected output
Plov is ready Dolma is ready Error: No dish ordered Kitchen closed
async function getScore() {
return 42;
}
const result = getScore();
console.log(result instanceof Promise);
result.then((score) => console.log('Score:', score));▸ Expected output
true Score: 42
Parallel tasks and fetch
If tasks don't depend on each other, waiting for them one by one wastes time. Promise.all starts all the promises at once and, when every one is ready, returns the results as an array in the same order. The three tasks below would take 600 ms one after another, but in parallel they take about 300 ms. If even one promise fails, Promise.all fails too.
const cook = (dish, ms) =>
new Promise((resolve) => setTimeout(() => resolve(`${dish} is ready`), ms));
async function breakfast() {
const results = await Promise.all([
cook('Tea', 300),
cook('Bread', 100),
cook('Jam', 200),
]);
console.log(results);
}
breakfast();▸ Expected output
[ 'Tea is ready', 'Bread is ready', 'Jam is ready' ]
In real projects, the async operation you will meet most often is loading data from a server with fetch. fetch returns a promise; when the response arrives, response.json() turns it into a JavaScript object — and that is a promise too. The example below is a template: the address is just a placeholder.
async function loadWeather(city) {
try {
const response = await fetch(`https://api.example.com/weather?city=${city}`);
if (!response.ok) {
throw new Error(`Server answered ${response.status}`);
}
const data = await response.json();
console.log(`${city}: ${data.temperature} °C`);
} catch (error) {
console.log('Could not load the weather:', error.message);
}
}
loadWeather('Baku');| Approach | Strength | Weakness |
|---|---|---|
| Callback | simple, works everywhere | unreadable when nested |
| Promise | chains, one catch for all errors | long chains are still hard to read |
| async/await | reads like normal code, try...catch | easy to forget await |
Write the async function countdown(from): it prints the numbers from from down to 1, waits 300 ms after each number with wait(300), and finally prints Go!. The await in the last line works here just as it does at the top level of a module.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function countdown(from) {
// print from, from - 1, ..., 1 with a 300 ms pause after each number
// then print Go!
}
await countdown(3);▸ Expected output
3 2 1 Go!
Key points
- Asynchronous code doesn't freeze the program while waiting;
setTimeoutschedules a function for later. - Ready callbacks run only after all synchronous code has finished (the event loop).
- A promise has three states: pending, fulfilled, rejected; take the result with
thenand the error withcatch. - An
asyncfunction always returns a promise;awaitwaits for the result, and errors are caught withtry...catch. Promise.allruns independent tasks in parallel and returns the results in the same order.
Check yourself
10 questions. Every correct answer earns XP.
console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C'); print?