Async Patterns
11 min read•Frontend Patterns & Concepts
Async Patterns
Asynchronous programming is fundamental to JavaScript. Understanding these patterns helps you write efficient, maintainable code that handles complex async scenarios gracefully.
Promise Fundamentals
Creating Promises
JAVASCRIPT
// Basic promise creation
const promise = new Promise((resolve, reject) => {
// Async operation
setTimeout(() => {
const success = true;
if (success) {
resolve("Operation completed");
} else {
reject(new Error("Operation failed"));
}
}, 1000);
});
// Promise states: pending → fulfilled or rejected
promise
.then((result) => console.log(result))
.catch((error) => console.error(error))
.finally(() => console.log("Cleanup"));Promisifying Callbacks
JAVASCRIPT
// Convert callback-based API to Promise
function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, "utf8", (error, data) => {
if (error) reject(error);
else resolve(data);
});
});
}
// Generic promisify utility
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
};
}
const readFile = promisify(fs.readFile);
const data = await readFile("file.txt", "utf8");Async/Await Patterns
Sequential Execution
JAVASCRIPT
async function processUserData(userId) {
try {
// Each await waits for the previous to complete
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts.map((p) => p.id));
return { user, posts, comments };
} catch (error) {
console.error("Failed to process user data:", error);
throw error;
}
}Parallel Execution
JAVASCRIPT
async function fetchDashboardData(userId) {
// All requests start simultaneously
const [user, notifications, stats] = await Promise.all([
fetchUser(userId),
fetchNotifications(userId),
fetchStats(userId),
]);
return { user, notifications, stats };
}
// With error handling for independent requests
async function fetchDashboardDataSafe(userId) {
const results = await Promise.allSettled([
fetchUser(userId),
fetchNotifications(userId),
fetchStats(userId),
]);
return {
user: results[0].status === "fulfilled" ? results[0].value : null,
notifications: results[1].status === "fulfilled" ? results[1].value : [],
stats: results[2].status === "fulfilled" ? results[2].value : null,
};
}Race Conditions
JAVASCRIPT
// First to complete wins
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const fetchPromise = fetch(url, { signal: controller.signal });
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
controller.abort();
reject(new Error("Request timed out"));
}, timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
// First successful result (Promise.any)
async function fetchFromMultipleSources(sources) {
try {
// Returns first successful response
return await Promise.any(sources.map((url) => fetch(url)));
} catch (error) {
// AggregateError if all fail
throw new Error("All sources failed");
}
}Error Handling Patterns
Try-Catch with Async/Await
JAVASCRIPT
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
console.log("Request was cancelled");
} else if (error.name === "TypeError") {
console.error("Network error or invalid URL");
} else {
console.error("Fetch error:", error.message);
}
throw error;
}
}Error Wrapping Utility
JAVASCRIPT
// Returns [error, result] tuple
async function to(promise) {
try {
const result = await promise;
return [null, result];
} catch (error) {
return [error, null];
}
}
// Usage - no try-catch needed
async function processUser(userId) {
const [userError, user] = await to(fetchUser(userId));
if (userError) {
return handleUserError(userError);
}
const [postsError, posts] = await to(fetchPosts(user.id));
if (postsError) {
return handlePostsError(postsError);
}
return { user, posts };
}Retry with Exponential Backoff
JAVASCRIPT
async function retryWithBackoff(fn, options = {}) {
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 10000,
shouldRetry = () => true,
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === maxRetries || !shouldRetry(error)) {
throw error;
}
// Exponential backoff with jitter
const delay = Math.min(
baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
maxDelay,
);
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await sleep(delay);
}
}
throw lastError;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Usage
const data = await retryWithBackoff(
() => fetch("/api/data").then((r) => r.json()),
{
maxRetries: 3,
shouldRetry: (error) => error.status !== 404, // Don't retry 404s
},
);Request Cancellation
AbortController
JAVASCRIPT
class CancellableRequest {
constructor() {
this.controller = null;
}
async fetch(url, options = {}) {
// Cancel previous request
this.cancel();
// Create new controller
this.controller = new AbortController();
try {
const response = await fetch(url, {
...options,
signal: this.controller.signal,
});
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
return null; // Cancelled, not an error
}
throw error;
}
}
cancel() {
if (this.controller) {
this.controller.abort();
this.controller = null;
}
}
}
// Usage in search
const searchRequest = new CancellableRequest();
async function handleSearchInput(query) {
// Previous search is automatically cancelled
const results = await searchRequest.fetch(`/api/search?q=${query}`);
if (results) {
displayResults(results);
}
}
// In React
function useSearch() {
const [results, setResults] = useState([]);
const controllerRef = useRef(null);
const search = useCallback(async (query) => {
// Cancel previous
controllerRef.current?.abort();
controllerRef.current = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: controllerRef.current.signal,
});
const data = await response.json();
setResults(data);
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
}
}
}, []);
// Cleanup on unmount
useEffect(() => {
return () => controllerRef.current?.abort();
}, []);
return { results, search };
}Concurrent Control Patterns
Rate Limiting
JAVASCRIPT
class RateLimiter {
constructor(requestsPerSecond) {
this.requestsPerSecond = requestsPerSecond;
this.tokens = requestsPerSecond;
this.lastRefill = Date.now();
this.queue = [];
}
async acquire() {
this.refill();
if (this.tokens > 0) {
this.tokens--;
return Promise.resolve();
}
// Wait for token
return new Promise((resolve) => {
this.queue.push(resolve);
setTimeout(() => this.processQueue(), 1000 / this.requestsPerSecond);
});
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.requestsPerSecond,
this.tokens + elapsed * this.requestsPerSecond,
);
this.lastRefill = now;
}
processQueue() {
this.refill();
while (this.tokens > 0 && this.queue.length > 0) {
this.tokens--;
const resolve = this.queue.shift();
resolve();
}
}
}
// Usage
const limiter = new RateLimiter(10); // 10 requests per second
async function rateLimitedFetch(url) {
await limiter.acquire();
return fetch(url);
}Semaphore (Concurrency Limit)
JAVASCRIPT
class Semaphore {
constructor(maxConcurrent) {
this.maxConcurrent = maxConcurrent;
this.current = 0;
this.queue = [];
}
async acquire() {
if (this.current < this.maxConcurrent) {
this.current++;
return Promise.resolve();
}
return new Promise((resolve) => {
this.queue.push(resolve);
});
}
release() {
this.current--;
if (this.queue.length > 0) {
this.current++;
const resolve = this.queue.shift();
resolve();
}
}
async run(fn) {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
}
// Process items with max 5 concurrent operations
async function processWithLimit(items, processor, limit = 5) {
const semaphore = new Semaphore(limit);
return Promise.all(items.map((item) => semaphore.run(() => processor(item))));
}
// Usage
const urls = ["url1", "url2" /* ...100 more */];
const results = await processWithLimit(
urls,
(url) => fetch(url).then((r) => r.json()),
5, // Max 5 concurrent requests
);Batching Requests
JAVASCRIPT
class RequestBatcher {
constructor(batchFn, options = {}) {
this.batchFn = batchFn;
this.maxBatchSize = options.maxBatchSize || 10;
this.maxWaitMs = options.maxWaitMs || 50;
this.queue = [];
this.timeout = null;
}
async request(item) {
return new Promise((resolve, reject) => {
this.queue.push({ item, resolve, reject });
if (this.queue.length >= this.maxBatchSize) {
this.flush();
} else if (!this.timeout) {
this.timeout = setTimeout(() => this.flush(), this.maxWaitMs);
}
});
}
async flush() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.maxBatchSize);
const items = batch.map((b) => b.item);
try {
const results = await this.batchFn(items);
batch.forEach((b, i) => b.resolve(results[i]));
} catch (error) {
batch.forEach((b) => b.reject(error));
}
}
}
// Usage: batch user fetches
const userBatcher = new RequestBatcher(
async (userIds) => {
// Single request for multiple users
const response = await fetch("/api/users", {
method: "POST",
body: JSON.stringify({ ids: userIds }),
});
return response.json();
},
{ maxBatchSize: 50, maxWaitMs: 10 },
);
// These get batched into one request
const [user1, user2, user3] = await Promise.all([
userBatcher.request("id1"),
userBatcher.request("id2"),
userBatcher.request("id3"),
]);Async Iteration
AsyncIterator for Pagination
JAVASCRIPT
async function* fetchAllPages(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
for (const item of data.items) {
yield item;
}
hasMore = data.hasNextPage;
page++;
}
}
// Usage with for-await
async function processAllItems(url) {
for await (const item of fetchAllPages(url)) {
await processItem(item);
}
}
// Collect all items
async function getAllItems(url) {
const items = [];
for await (const item of fetchAllPages(url)) {
items.push(item);
}
return items;
}Streaming with AsyncIterator
JAVASCRIPT
async function* streamResponse(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value, { stream: true });
}
} finally {
reader.releaseLock();
}
}
// Process streaming response
async function processStream(url) {
const response = await fetch(url);
let buffer = "";
for await (const chunk of streamResponse(response)) {
buffer += chunk;
// Process complete lines
const lines = buffer.split("\n");
buffer = lines.pop(); // Keep incomplete line
for (const line of lines) {
if (line.trim()) {
const data = JSON.parse(line);
handleData(data);
}
}
}
}Event-Driven Async
Promise-based Event Handling
JAVASCRIPT
function once(emitter, event) {
return new Promise((resolve, reject) => {
const handleSuccess = (data) => {
emitter.off("error", handleError);
resolve(data);
};
const handleError = (error) => {
emitter.off(event, handleSuccess);
reject(error);
};
emitter.once(event, handleSuccess);
emitter.once("error", handleError);
});
}
// Usage
const data = await once(socket, "message");
// With timeout
async function onceWithTimeout(emitter, event, timeout) {
return Promise.race([
once(emitter, event),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeout),
),
]);
}Async Queue
JAVASCRIPT
class AsyncQueue {
constructor() {
this.queue = [];
this.waiters = [];
}
enqueue(item) {
if (this.waiters.length > 0) {
const waiter = this.waiters.shift();
waiter.resolve(item);
} else {
this.queue.push(item);
}
}
async dequeue() {
if (this.queue.length > 0) {
return this.queue.shift();
}
return new Promise((resolve) => {
this.waiters.push({ resolve });
});
}
async *[Symbol.asyncIterator]() {
while (true) {
yield await this.dequeue();
}
}
}
// Producer-consumer pattern
const queue = new AsyncQueue();
// Producer
async function producer() {
for (let i = 0; i < 10; i++) {
await sleep(100);
queue.enqueue({ id: i, data: `Item ${i}` });
}
}
// Consumer
async function consumer() {
for await (const item of queue) {
console.log("Processing:", item);
// Process item
}
}React Async Patterns
useAsync Hook
JAVASCRIPT
function useAsync(asyncFn, deps = []) {
const [state, setState] = useState({
data: null,
loading: true,
error: null,
});
const execute = useCallback(async () => {
setState((s) => ({ ...s, loading: true, error: null }));
try {
const data = await asyncFn();
setState({ data, loading: false, error: null });
return data;
} catch (error) {
setState({ data: null, loading: false, error });
throw error;
}
}, deps);
useEffect(() => {
execute();
}, [execute]);
return { ...state, execute };
}
// Usage
function UserProfile({ userId }) {
const {
data: user,
loading,
error,
execute: refetch,
} = useAsync(() => fetchUser(userId), [userId]);
if (loading) return <Spinner />;
if (error) return <Error error={error} onRetry={refetch} />;
return <Profile user={user} />;
}Suspense for Data Fetching
JAVASCRIPT
// Resource wrapper for Suspense
function wrapPromise(promise) {
let status = "pending";
let result;
const suspender = promise.then(
(r) => {
status = "success";
result = r;
},
(e) => {
status = "error";
result = e;
},
);
return {
read() {
if (status === "pending") throw suspender;
if (status === "error") throw result;
return result;
},
};
}
// Create resource
const userResource = wrapPromise(fetchUser(userId));
// Component reads synchronously
function UserProfile() {
const user = userResource.read();
return <Profile user={user} />;
}
// Usage with Suspense
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>;Summary
| Pattern | Use Case | Key Benefit |
|---|---|---|
| Promise.all | Parallel independent tasks | Maximum parallelism |
| Promise.allSettled | Parallel with fault tolerance | No short-circuit on error |
| Promise.race | Timeout, fastest source | First result wins |
| Promise.any | Redundant sources | First success wins |
| AbortController | Cancellation | Clean resource cleanup |
| Retry with backoff | Transient failures | Resilient requests |
| Semaphore | Concurrency limit | Prevent overload |
| Batching | Many small requests | Reduce overhead |
| AsyncIterator | Pagination, streams | Memory efficient |
Key Principles:
- Handle errors at appropriate levels
- Always consider cancellation
- Limit concurrency to prevent resource exhaustion
- Use timeouts for external calls
- Clean up resources in finally blocks