Asynchronous JavaScript
Asynchronous programming in JavaScript allows tasks to run without blocking the main thread. This is essential for network requests, timers, file I/O, and any operation that would freeze the UI if executed synchronously.
Synchronous vs Asynchronous
console.log('Start');
setTimeout(() => {
console.log('Async task done');
}, 1000);
console.log('End');
// Output: Start, End, Async task done (after 1 second)
JavaScript is single-threaded — one call stack. Async operations delegate to the browser (Web APIs) and return via the callback queue processed by the event loop.
Callback Functions
A callback is a function passed to another function, invoked when an async operation completes:
function fetchData(callback) {
setTimeout(() => {
console.log('Data fetched');
callback(null, { id: 1, name: 'Alice' });
}, 1000);
}
function displayData(err, data) {
if (err) return console.error(err);
console.log('Display:', data);
}
fetchData(displayData);
Callback Hell
Nested callbacks become hard to read and error-prune:
getUser(1, (err, user) => {
getOrders(user.id, (err, orders) => {
getOrderDetails(orders[0].id, (err, details) => {
// deeply nested...
});
});
});
Promises and async/await solve this.
Promises
A Promise represents a future value — pending, fulfilled, or rejected:
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('Operation successful');
} else {
reject(new Error('Operation failed'));
}
});
promise
.then(message => console.log(message))
.catch(error => console.error(error.message))
.finally(() => console.log('Cleanup always runs'));
Chaining Promises
fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => fetchDetails(orders[0].id))
.then(details => console.log(details))
.catch(err => console.error('Any step failed:', err));
Return a value or Promise from .then() to chain. Throw or return rejected Promise to trigger .catch().
Promise Utilities
// All must succeed
Promise.all([fetchUser(), fetchPosts()])
.then(([user, posts]) => console.log(user, posts))
.catch(err => console.error('One failed:', err));
// All settle — never rejects
Promise.allSettled([fetchA(), fetchB()])
.then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.log(r.reason);
});
});
// First to resolve wins
Promise.race([fetchFromCDN1(), fetchFromCDN2()])
.then(data => console.log('Fastest:', data));
// First fulfilled (ignores rejections until all fail)
Promise.any([fetchMirror1(), fetchMirror2()])
.then(data => console.log(data));
Async/Await
Syntax sugar over Promises — makes async code read like synchronous:
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => resolve('Data fetched'), 1000);
});
}
async function displayData() {
const data = await fetchData();
console.log(data);
}
displayData();
async functions always return a Promise. await pauses within the async function until Promise settles.
Error Handling
async function loadUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to load user:', error.message);
throw error; // re-throw for caller to handle
}
}
Prefer try/catch around await — cleaner than .catch() chains in async functions.
Sequential vs Parallel Execution
async function sequential() {
const user = await fetchUser(); // wait
const orders = await fetchOrders(user.id); // then wait
return orders;
}
async function parallel() {
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);
return { user, posts };
}
Use parallel when tasks are independent — often 2–10× faster than sequential.
Fetch API Example
async function getJSON(url) {
const response = await fetch(url, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) throw new Error(response.statusText);
return response.json();
}
getJSON('/api/data')
.then(data => console.log(data))
.catch(err => console.error(err));
Common Patterns
Timeout Wrapper
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
}
await withTimeout(fetch('/api/slow'), 5000);
Retry with Backoff
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fetch(url);
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
Best Practices
- Always handle errors — unhandled Promise rejections crash Node and warn in browsers
- Use
Promise.allfor independent tasks — don’t await in a loop unnecessarily - Avoid mixing callbacks and Promises — wrap callbacks with
new Promise - Don’t await in loops when parallel is possible — use
Promise.all(items.map(...)) - Understand the event loop — see Event Loop chapter
Troubleshooting
await syntax error
awaitonly valid insideasyncfunctions (or top-level in modules)
Promise never resolves
- Missing
resolve()/reject()call - Forgot to
returnPromise from function
Race conditions
- Multiple async updates to same state — use abort controllers or sequential queue
Unhandled rejection
- Add
.catch()or wrap intry/catch
Async JavaScript powers modern web apps — master Promises and async/await for clean, maintainable asynchronous code.