LEARN TO CODE | JAVASCRIPT | 04 APIS AND MINI PROJECTS
04.1 Promises, async and await
Follow an asynchronous operation from its pending promise to a result or failure without confusing waiting with freezing the page.
01 | SOME RESULTS ARE NOT AVAILABLE IMMEDIATELY
A calculation such as 3 * 6 can produce its result straight away. Asking another computer for a file is different: the request takes time and might fail. The page should remain usable while it waits.
Asynchronous code coordinates work whose result arrives later. Instead of blocking everything with a busy waiting loop, the browser can continue processing other events and resume the waiting code when appropriate.
We will start with a local timer, which makes the timing easy to observe without needing internet access. The next lesson replaces that simulated wait with a fetch request.
Use VS Code to edit index.html, script.js and style.css in one project folder. Link the external script with defer. Save changes and refresh the browser; inspect both the visible page and the Console.
The complete download has a separate folder for each project. Keep the examples independent to avoid colliding top-level variable names.
02 | A PROMISE REPRESENTS AN OUTCOME
| State | Meaning |
|---|---|
| pending | The outcome is not available yet |
| fulfilled | The promise completed successfully with a value |
| rejected | The promise completed unsuccessfully with a reason |
A promise is an object representing an eventual outcome. It is not the result value itself. A promise that will eventually provide a club list is different from that array of clubs.
Once settled, that promise does not switch back to pending. To start a new independent attempt, the code usually calls the operation again and receives another promise.
“Resolved” has a more technical meaning involving adoption of other promises. For now, use pending, fulfilled and rejected when describing the observed state.
03 | A SMALL PROMISE YOU CAN FOLLOW
function getMessageLater() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Message ready");
}, 1000);
});
}
The function returns a new Promise. The function passed to the Promise constructor, called the executor, runs immediately and arranges a timer callback. The timer calls resolve with a value later.
setTimeout schedules a callback after at least the requested delay, subject to browser scheduling. It is not a precise stopwatch and does not itself return a promise.
This is a teaching example of constructing a promise. Browser APIs such as fetch already return promises, so you normally use those directly rather than wrapping them in a second Promise.
04 | THEN RECEIVES A FULFILLED VALUE
function getMessageLater() {
return new Promise((resolve) => {
setTimeout(() => resolve("Message ready"), 1000);
});
}
console.log("Start");
getMessageLater().then((message) => {
console.log(message);
});
console.log("End of current script");
The immediate output is Start, followed by End of current script. Later, the callback logs Message ready. Registering .then does not pause the rest of the current script.
The callback receives the fulfilled value. .then returns a new promise, allowing a chain of later operations. Returning a value or promise from a chain callback passes its outcome along; forgetting that return can break a dependent sequence.
05 | ASYNC FUNCTIONS RETURN PROMISES
async function getNumber() {
return 7;
}
getNumber().then((number) => console.log(number));
async marks a function whose call returns a promise. Even though this function returns the number 7 in its body, its caller receives a promise fulfilled with 7.
Writing const result = getNumber() does not make result the number 7. To use the fulfilled value, a caller must await the promise within an appropriate async context, or use a promise callback.
async does not automatically move calculations to a background thread. Long synchronous work inside an async function can still make the page unresponsive.
06 | AWAIT PAUSES THIS FUNCTION’S CONTINUATION
function getMessageLater() {
return new Promise((resolve) => {
setTimeout(() => resolve("Message ready"), 1000);
});
}
async function showMessage() {
console.log("Inside: before await");
const message = await getMessageLater();
console.log(message);
}
showMessage();
console.log("Outside: the caller continued");
The function starts and logs its first message. At await, its continuation is suspended until the outcome is available. The caller can continue, so Outside is logged before Message ready.
await is used inside async functions in these classic scripts. Top-level await belongs to module contexts, which are outside this starter setup.
It pauses this function’s dependent work, not the whole browser. The browser still needs chances to run its event loop; a tight while loop does not become responsive merely because it is inside an async function.
07 | REJECTIONS NEED A ROUTE
An operation can reject its promise instead of supplying a successful value. When you await a rejection, it behaves like a thrown exception at that await point. A surrounding try/catch can handle it.
async function demonstrateFailure() {
try {
const value = await Promise.reject(new Error("Practice failure"));
console.log(value);
} catch (error) {
console.log(`Handled: ${error.message}`);
} finally {
console.log("Cleanup complete");
}
}
demonstrateFailure();
The successful log of value is skipped. catch handles the failure; finally runs afterwards for cleanup on either route. A .catch callback is the promise-chain alternative.
Disabling a button while work runs and re-enabling it in finally prevents the interface being left stuck after failure. Expected failures should not become unhandled rejections in the Console.
08 | COMPLETE RESPONSIVE WAIT DEMO
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wait without freezing the page</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>Wait without freezing the page</h1>
<p>This local timer simulates waiting. No internet request is made.</p>
<p><label><input id="async-fail" type="checkbox"> Simulate a failed operation</label></p>
<button id="async-start" type="button">Start waiting</button>
<button id="async-count" type="button">Count a click while waiting</button>
<p id="async-clicks" role="status">Other clicks: 0</p>
<p id="async-status" role="status" aria-live="polite">Ready.</p>
<ol id="async-log"></ol>
</main>
</body>
</html>
script.js
const startButton = document.querySelector("#async-start");
const failureInput = document.querySelector("#async-fail");
const status = document.querySelector("#async-status");
const log = document.querySelector("#async-log");
let running = false;
let otherClicks = 0;
function addLog(message) {
const item = document.createElement("li");
item.textContent = message;
log.append(item);
}
function waitForMessage(shouldFail) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error("The simulated operation failed."));
} else {
resolve("Your practice result is ready.");
}
}, 1200);
});
}
async function runDemo() {
if (running) return;
running = true;
startButton.disabled = true;
const shouldFail = failureInput.checked;
log.replaceChildren();
status.textContent = "Waiting... try the other button.";
addLog("1. Started the async function.");
try {
const message = await waitForMessage(shouldFail);
status.textContent = message;
addLog("3. The awaited result is available.");
} catch (error) {
status.textContent = error.message;
addLog("3. The rejection reached catch.");
} finally {
running = false;
startButton.disabled = false;
addLog("4. Cleanup finished; Start is available again.");
}
}
startButton.addEventListener("click", () => {
runDemo(); // runDemo handles its expected rejection internally.
addLog("2. The click listener continued while the function awaited.");
});
document.querySelector("#async-count").addEventListener("click", () => {
otherClicks += 1;
document.querySelector("#async-clicks").textContent = `Other clicks: ${otherClicks}`;
});
09 | OBSERVE BOTH ROUTES
This local timer simulates waiting. No internet request is made.
Other clicks: 0
Ready.
Start the operation, then activate the other button while waiting. The counter should remain usable. Repeat with Simulate a failed operation selected.
The flag is captured at the start of the operation. Changing the checkbox mid-wait does not change the already-started simulation. This keeps the example predictable.
TRY IT YOURSELF
Why does log item 2 appear before item 3, despite being written after runDemo() at the call site?
Reveal guidance
runDemo reaches await and yields its continuation. The click listener continues and adds item 2 before the awaited timer promise settles.
10 | SEQUENCE AND CONCURRENCY
If task B depends on task A’s result, awaiting A before B expresses that dependency. If two tasks are independent, starting them together may make sense.
async function loadBoth() {
const first = Promise.resolve("First");
const second = Promise.resolve("Second");
const results = await Promise.all([first, second]);
console.log(results.join(", "));
}
loadBoth();
Promise.all waits for both results and returns them in the input order. It rejects if one input rejects; it does not automatically cancel other operations that are already running.
This is a first look, not a reason to make every operation concurrent. Choose an order that matches the dependencies and the service’s limits.
11 | EXPLAIN THE TIMELINE
TRY IT YOURSELF
Describe the demo from click to cleanup, using pending, fulfilled or rejected, await, catch and finally.
Reveal guidance
The click starts runDemo, which starts a timer-backed pending promise. The function awaits it while other event handlers can run. Fulfilment resumes the success path; rejection enters catch. finally restores the Start control on either route.
Next, use this same waiting structure to request data with fetch.
KEEP LEARNING
Previous page 04.2 Fetch, JSON and error handling
Download Section 04 example programs
Further reading: Promises · Fetch · JSON · Browser storage