Asynchronous programming is essential for modern JavaScript. This guide covers async patterns, from callbacks to async/await, and best practices for handling asynchronous operations.
Callbacks #
Traditional approach to async programming.
function fetchData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status === 200) {
callback(null, JSON.parse(xhr.responseText));
} else {
callback(new Error('Request failed'));
}
};
xhr.send();
}
// Usage
fetchData('/api/users', (error, data) => {
if (error) {
console.error(error);
return;
}
console.log(data);
});Callback Hell #
// Nested callbacks - hard to read
fetchUser((err, user) => {
if (err) return handleError(err);
fetchPosts(user.id, (err, posts) => {
if (err) return handleError(err);
fetchComments(posts[0].id, (err, comments) => {
if (err) return handleError(err);
console.log(comments);
});
});
});Promises #
Better abstraction for async operations.
function fetchData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('Request failed'));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send();
});
}
// Usage
fetchData('/api/users')
.then(data => console.log(data))
.catch(error => console.error(error));Promise Chaining #
fetchUser()
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments))
.catch(error => console.error(error));Promise.all() #
Run promises in parallel.
Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
])
.then(users => {
console.log('All users:', users);
})
.catch(error => {
console.error('At least one failed:', error);
});Promise.allSettled() #
Wait for all promises, regardless of outcome.
Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(3)
])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`User ${index + 1}:`, result.value);
} else {
console.error(`User ${index + 1} failed:`, result.reason);
}
});
});Promise.race() #
First promise to settle wins.
Promise.race([
fetchFromServer1(),
fetchFromServer2(),
fetchFromServer3()
])
.then(data => {
console.log('Fastest server:', data);
})
.catch(error => {
console.error('All failed:', error);
});Promise.any() #
First promise to fulfill wins.
Promise.any([
fetchFromServer1(),
fetchFromServer2(),
fetchFromServer3()
])
.then(data => {
console.log('First successful:', data);
})
.catch(error => {
console.error('All rejected:', error);
});Async/Await #
Syntactic sugar over promises.
async function fetchUserData() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return comments;
} catch (error) {
console.error('Error:', error);
}
}
// Usage
fetchUserData().then(comments => {
console.log(comments);
});Error Handling #
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch:', error);
throw error;
}
}Parallel Execution #
// Sequential - Slow
async function sequential() {
const user1 = await fetchUser(1); // Wait
const user2 = await fetchUser(2); // Wait
const user3 = await fetchUser(3); // Wait
return [user1, user2, user3];
}
// Parallel - Fast
async function parallel() {
const [user1, user2, user3] = await Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
return [user1, user2, user3];
}Advanced Patterns #
Retry Logic #
async function retry(fn, maxAttempts = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await sleep(delay);
delay *= 2; // Exponential backoff
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Usage
const data = await retry(() => fetchData('/api/users'));Timeout #
function timeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
]);
}
// Usage
try {
const data = await timeout(fetchData('/api/slow'), 5000);
console.log(data);
} catch (error) {
console.error('Request timed out');
}Rate Limiting #
class RateLimiter {
constructor(maxConcurrent) {
this.maxConcurrent = maxConcurrent;
this.running = 0;
this.queue = [];
}
async run(fn) {
while (this.running >= this.maxConcurrent) {
await new Promise(resolve => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
const resolve = this.queue.shift();
if (resolve) resolve();
}
}
}
// Usage
const limiter = new RateLimiter(3); // Max 3 concurrent
const requests = urls.map(url =>
limiter.run(() => fetch(url))
);
const results = await Promise.all(requests);Queue #
class AsyncQueue {
constructor() {
this.queue = [];
this.processing = false;
}
async add(fn) {
this.queue.push(fn);
if (!this.processing) {
await this.process();
}
}
async process() {
this.processing = true;
while (this.queue.length > 0) {
const fn = this.queue.shift();
try {
await fn();
} catch (error) {
console.error('Task failed:', error);
}
}
this.processing = false;
}
}
// Usage
const queue = new AsyncQueue();
queue.add(async () => {
console.log('Task 1');
await sleep(1000);
});
queue.add(async () => {
console.log('Task 2');
await sleep(500);
});Debounce Async #
function debounceAsync(fn, delay) {
let timeoutId;
let latestResolve;
let latestReject;
return function (...args) {
clearTimeout(timeoutId);
return new Promise((resolve, reject) => {
latestResolve = resolve;
latestReject = reject;
timeoutId = setTimeout(async () => {
try {
const result = await fn.apply(this, args);
latestResolve(result);
} catch (error) {
latestReject(error);
}
}, delay);
});
};
}
// Usage
const debouncedSearch = debounceAsync(async (query) => {
const results = await fetch(`/api/search?q=${query}`);
return results.json();
}, 300);
// Only last call executes
debouncedSearch('java');
debouncedSearch('javascript'); // This one executes
Memoization with Async #
function memoizeAsync(fn) {
const cache = new Map();
const pending = new Map();
return async function (...args) {
const key = JSON.stringify(args);
// Return cached result
if (cache.has(key)) {
return cache.get(key);
}
// Return pending promise
if (pending.has(key)) {
return pending.get(key);
}
// Execute and cache
const promise = fn.apply(this, args);
pending.set(key, promise);
try {
const result = await promise;
cache.set(key, result);
return result;
} finally {
pending.delete(key);
}
};
}
// Usage
const fetchUser = memoizeAsync(async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
// First call - hits API
const user1 = await fetchUser(1);
// Second call - returns cached
const user2 = await fetchUser(1);Parallel Limit #
async function parallelLimit(tasks, limit) {
const results = [];
const executing = [];
for (const [index, task] of tasks.entries()) {
const promise = Promise.resolve().then(() => task());
results[index] = promise;
if (limit <= tasks.length) {
const e = promise.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
// Usage
const tasks = urls.map(url => () => fetch(url));
const results = await parallelLimit(tasks, 5); // Max 5 concurrent
Cancel Token #
class CancelToken {
constructor() {
this.cancelled = false;
this.promise = new Promise((resolve) => {
this.cancel = () => {
this.cancelled = true;
resolve();
};
});
}
throwIfCancelled() {
if (this.cancelled) {
throw new Error('Operation cancelled');
}
}
}
async function fetchWithCancel(url, cancelToken) {
cancelToken.throwIfCancelled();
const response = await fetch(url);
cancelToken.throwIfCancelled();
const data = await response.json();
return data;
}
// Usage
const token = new CancelToken();
setTimeout(() => token.cancel(), 1000);
try {
const data = await fetchWithCancel('/api/slow', token);
console.log(data);
} catch (error) {
console.log('Cancelled');
}AbortController #
const controller = new AbortController();
const { signal } = controller;
// Abort after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('/api/data', { signal });
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted');
}
}Async Iterators #
Async Generator #
async function* fetchPages(url) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${url}?page=${page}`);
const data = await response.json();
yield data.items;
hasMore = data.hasMore;
page++;
}
}
// Usage
for await (const items of fetchPages('/api/items')) {
console.log('Page items:', items);
}Async Iterable #
class AsyncDataSource {
constructor(url) {
this.url = url;
}
async *[Symbol.asyncIterator]() {
let page = 1;
while (true) {
const response = await fetch(`${this.url}?page=${page}`);
const data = await response.json();
if (data.items.length === 0) break;
for (const item of data.items) {
yield item;
}
page++;
}
}
}
// Usage
const dataSource = new AsyncDataSource('/api/users');
for await (const user of dataSource) {
console.log(user);
}Event Emitters #
Custom Event Emitter #
class AsyncEventEmitter {
constructor() {
this.events = new Map();
}
on(event, listener) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(listener);
}
async emit(event, data) {
const listeners = this.events.get(event) || [];
for (const listener of listeners) {
await listener(data);
}
}
off(event, listener) {
const listeners = this.events.get(event);
if (listeners) {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
}
// Usage
const emitter = new AsyncEventEmitter();
emitter.on('data', async (data) => {
await processData(data);
});
await emitter.emit('data', { id: 1 });Observables #
Simple Observable #
class Observable {
constructor(subscriber) {
this.subscriber = subscriber;
}
subscribe(observer) {
return this.subscriber(observer);
}
static from(iterable) {
return new Observable((observer) => {
for (const value of iterable) {
observer.next(value);
}
observer.complete();
});
}
map(fn) {
return new Observable((observer) => {
return this.subscribe({
next: (value) => observer.next(fn(value)),
error: (err) => observer.error(err),
complete: () => observer.complete()
});
});
}
}
// Usage
const numbers = Observable.from([1, 2, 3, 4, 5]);
const doubled = numbers.map(x => x * 2);
doubled.subscribe({
next: (value) => console.log(value),
complete: () => console.log('Done')
});Best Practices #
Always Handle Errors #
// Bad
async function fetchData() {
const response = await fetch('/api/data');
return response.json();
}
// Good
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
}Don’t Await Unnecessarily #
// Bad - Unnecessary await
async function getUser(id) {
return await fetchUser(id);
}
// Good - Return promise directly
async function getUser(id) {
return fetchUser(id);
}
// Only await if you need the value
async function getUserName(id) {
const user = await fetchUser(id);
return user.name;
}Use Promise.all for Independence #
// Bad - Sequential
async function getData() {
const users = await fetchUsers();
const posts = await fetchPosts();
return { users, posts };
}
// Good - Parallel
async function getData() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
return { users, posts };
}Avoid Async in Constructors #
// Bad
class User {
constructor(id) {
this.data = await fetchUser(id); // Can't use await here
}
}
// Good
class User {
constructor(data) {
this.data = data;
}
static async create(id) {
const data = await fetchUser(id);
return new User(data);
}
}
// Usage
const user = await User.create(1);Summary #
- Prefer async/await - More readable than promises
- Handle all errors - Use try/catch
- Parallel when possible - Use Promise.all
- Add timeouts - Prevent hanging
- Implement retries - For unreliable operations
- Rate limit - Avoid overwhelming APIs
- Cancel when needed - Use AbortController
- Test async code - Use proper test frameworks
Async patterns are essential for modern JavaScript. Master these techniques to write efficient, maintainable asynchronous code.