JavaScript Async Programming
JavaScript is single-threaded — it can only do one thing at a time. But if a network request takes 3 seconds, should the page freeze for 3 seconds? Of course not. Async programming solves this: instead of waiting around, you take a number and come back when it's ready. Like a restaurant queue system — you don't stand at the kitchen door, you sit down and wait for your number to be called.
Sync vs Async
Synchronous: Code executes line by line — the next line waits for the current one to finish. Like standing in line — you wait until the person ahead of you is done.
Asynchronous: Certain operations (network requests, timers, file I/O) start without blocking subsequent code. When they finish, you're notified via a callback or Promise.
<script>
console.log('1');
setTimeout(function() {
console.log('2');
}, 1000);
console.log('3');
</script>
The output order is 1 → 3 → 2. setTimeout is asynchronous — its callback runs after 1 second without blocking console.log('3').
Callbacks
Callbacks are the most basic async pattern — pass a function as an argument, and it gets called when the async operation completes.
Example: Basic Callback Usage
<div id="output" style="padding: 10px; border: 1px solid #ccc;"></div>
<script>
const output = document.getElementById('output');
function fetchData(callback) {
output.textContent = 'Loading...';
setTimeout(function() {
callback('Data loaded!');
}, 2000);
}
fetchData(function(data) {
output.textContent = data;
});
</script>
Callback Hell
When async operations depend on each other in sequence, callbacks nest deeper and deeper — this is the infamous "Callback Hell."
<script>
getUser(function(user) {
getOrders(user.id, function(orders) {
getOrderDetail(orders[0].id, function(detail) {
getItems(detail.itemId, function(items) {
console.log('4 levels deep — add more complexity and it becomes unreadable');
});
});
});
});
</script>
Promise
A Promise is an object that represents the eventual result of an async operation. It has three states:
| State | Description |
|---|---|
pending |
In progress, no result yet |
fulfilled |
Completed successfully, result available |
rejected |
Failed, error available |
Once a Promise transitions from pending to fulfilled or rejected, it never changes back — like an arrow once shot, it can't be recalled.
Creating a Promise
<script>
const promise = new Promise(function(resolve, reject) {
// async operation
// call resolve(result) on success
// call reject(error) on failure
});
</script>
then / catch / finally
<script>
promise
.then(function(result) { console.log(result); }) // runs on success
.catch(function(error) { console.error(error); }) // runs on failure
.finally(function() { console.log('Done'); }); // runs regardless
</script>
Example: Simulating a Request with Promise
<button id="loadBtn">Load Data</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');
function fetchUser() {
return new Promise(function(resolve, reject) {
output.textContent = 'Loading...';
setTimeout(function() {
const success = true;
if (success) {
resolve({ name: 'Alice', age: 25 });
} else {
reject(new Error('Load failed'));
}
}, 1500);
});
}
btn.addEventListener('click', function() {
fetchUser()
.then(function(user) {
output.textContent = 'Name: ' + user.name + ', Age: ' + user.age;
})
.catch(function(err) {
output.textContent = 'Error: ' + err.message;
})
.finally(function() {
console.log('Request completed (success or failure)');
});
});
</script>
Chaining
then itself returns a Promise, so you can chain calls — callback hell becomes a straight line.
<script>
fetchUser()
.then(function(user) { return fetchOrders(user.id); })
.then(function(orders) { return fetchDetail(orders[0].id); })
.then(function(detail) { console.log(detail); })
.catch(function(err) { console.error(err); });
</script>
catch at the end of the chain catches errors from any step — you don't need a catch after every then.
async / await
async/await is syntactic sugar over Promises — it makes async code look synchronous, greatly improving readability.
async: Declares an async function that automatically returns a Promiseawait: Pauses execution until the Promise resolves, then returns the result
Example: Rewriting the Above Request with async/await
<button id="loadBtn">Load Data</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');
function fetchUser() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ name: 'Bob', age: 30 });
}, 1500);
});
}
btn.addEventListener('click', async function() {
try {
output.textContent = 'Loading...';
const user = await fetchUser();
output.textContent = 'Name: ' + user.name + ', Age: ' + user.age;
} catch (err) {
output.textContent = 'Error: ' + err.message;
} finally {
console.log('Request completed');
}
});
</script>
async/await turns "take a number and wait" into "stay in line without leaving" — the code flow is straight and readability skyrockets.
Error Handling
async/await uses try/catch for error handling — the same pattern used with synchronous code.
<script>
async function loadAll() {
try {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
console.log(orders);
} catch (err) {
console.error('Something went wrong:', err);
}
}
</script>
Promise.all and Promise.race
| Method | Description |
|---|---|
Promise.all([p1, p2, p3]) |
Succeeds only if all succeed; fails as soon as one fails |
Promise.race([p1, p2, p3]) |
Uses the result of whichever settles first (success or failure) |
Example: Concurrent Requests with Promise.all
<button id="loadAll">Load Concurrently</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadAll');
const output = document.getElementById('output');
function delay(ms, value) {
return new Promise(function(resolve) {
setTimeout(function() { resolve(value); }, ms);
});
}
btn.addEventListener('click', async function() {
output.textContent = 'Loading...';
const results = await Promise.all([
delay(1000, 'User Data'),
delay(1500, 'Order Data'),
delay(800, 'Config Data')
]);
output.textContent = 'All loaded:\n' + results.join(' | ');
});
const raceBtn = document.createElement('button');
raceBtn.textContent = 'Race Load';
document.body.appendChild(raceBtn);
raceBtn.addEventListener('click', async function() {
const fastest = await Promise.race([
delay(1000, 'Source A (1s)'),
delay(500, 'Source B (0.5s)'),
delay(800, 'Source C (0.8s)')
]);
output.textContent = 'Fastest: ' + fastest;
});
</script>
Promise.all is like a group dinner — everyone must arrive before you eat. Promise.race is like a race — whoever finishes first wins. Use all for independent requests to speed things up, and race when you have multiple mirror sources and want the fastest one.
📖 Summary
- Sync code blocks subsequent execution; async code doesn't — single-threaded can still "do multiple things"
- Callbacks are the most basic async pattern; deep nesting creates callback hell
- Promise has three states:
pending → fulfilledorpending → rejected— state transitions are irreversible then/catch/finallyconsume Promises; chaining solves callback hellasync/awaitis syntactic sugar over Promises;awaitcan only be used insideasyncfunctionsPromise.allwaits for all to succeed;Promise.racetakes the first to settle
❓ FAQ
Q: Can
awaitbe used in a regular function? A: No.awaitcan only be used inside anasyncfunction. Usingawaitin a regular function causes a syntax error. Top-levelawaitis supported in modules, but browser support varies.
Q: What happens if one of the Promises in
Promise.allfails? A:Promise.allimmediately rejects with the first error. Other Promises still execute, but their results are ignored. If you need "all settled regardless of outcome," usePromise.allSettled.
Q: What does an
asyncfunction return? A: Anasyncfunction always returns a Promise. Even if youreturn 42, it actually returnsPromise.resolve(42). So you can use.then()to consume the result.
📝 Exercises
- Basic: Use
Promise+setTimeoutto simulate a 2-second delay, then output "Done" on success. - Intermediate: Rewrite the above with
async/awaitand addtry/catcherror handling. - Challenge: Use
Promise.allto fire 3 simulated requests with different delays (1s/2s/3s) concurrently, then calculate and display the total elapsed time when all complete.



