JavaScript Promises & Async/Await Guide
A complete reference for asynchronous JavaScript. Master Promises, async/await syntax, error handling, and practical patterns for API calls, parallel execution, and more.
Callbacks (The Old Way)
Understanding callbacks helps appreciate why Promises and async/await are better.
Basic Callback
// Callback-based async code
function fetchUser(id, callback) {
setTimeout(() => {
callback({ id: id, name: 'John Doe' });
}, 1000);
}
// Usage
fetchUser(1, (user) => {
console.log(user);
});
Callback Hell (Pyramid of Doom)
// ❌ Nested callbacks = hard to read and maintain
fetchUser(1, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
fetchLikes(comments[0].id, (likes) => {
console.log(likes);
// Keep nesting... 😱
});
});
});
});
Callbacks are hard to chain, difficult to handle errors, and create deeply nested code.
Promises
A Promise represents a value that may be available now, in the future, or never.
Promise States
// A Promise has three states:
// 1. pending - initial state
// 2. fulfilled - operation completed successfully
// 3. rejected - operation failed
const promise = new Promise((resolve, reject) => {
// pending...
if (success) {
resolve(value); // fulfilled
} else {
reject(error); // rejected
}
});
Creating a Promise
// Basic Promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
// Promise with conditional logic
function fetchData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url) {
resolve({ data: 'Some data' });
} else {
reject(new Error('URL is required'));
}
}, 1000);
});
}
// Immediately resolved Promise
const resolvedPromise = Promise.resolve('Immediate value');
// Immediately rejected Promise
const rejectedPromise = Promise.reject(new Error('Failed'));
Using Promises (.then)
// Handling success
myPromise
.then((result) => {
console.log(result); // 'Success!'
});
// Handling errors
myPromise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error);
});
// Finally block (runs regardless of outcome)
myPromise
.then((result) => console.log(result))
.catch((error) => console.error(error))
.finally(() => {
console.log('Cleanup or loading state reset');
});
Promise Chaining
// ✅ Much better than callbacks!
fetchUser(1)
.then((user) => {
console.log('User:', user);
return fetchPosts(user.id);
})
.then((posts) => {
console.log('Posts:', posts);
return fetchComments(posts[0].id);
})
.then((comments) => {
console.log('Comments:', comments);
})
.catch((error) => {
console.error('Error:', error);
})
.finally(() => {
console.log('All done!');
});
// Returning values in chain
Promise.resolve(5)
.then((num) => num * 2) // 10
.then((num) => num + 3) // 13
.then((num) => {
console.log(num); // 13
});
Async/Await
Modern syntax that makes asynchronous code look and behave like synchronous code.
Async Functions
// async keyword makes a function return a Promise
async function fetchData() {
return 'Hello';
}
// Equivalent to:
function fetchData() {
return Promise.resolve('Hello');
}
// Usage
fetchData().then((result) => console.log(result)); // 'Hello'
// Arrow function
const getData = async () => {
return 'Data';
};
Await Keyword
// await pauses execution until Promise resolves
async function getUser() {
const response = await fetch('/api/user');
const user = await response.json();
return user;
}
// ✅ Clean, readable code
async function fetchAllData() {
const user = await fetchUser(1);
console.log('User:', user);
const posts = await fetchPosts(user.id);
console.log('Posts:', posts);
const comments = await fetchComments(posts[0].id);
console.log('Comments:', comments);
return comments;
}
// await only works inside async functions
async function example() {
const data = await fetchData(); // ✅ Works
}
// ❌ This won't work
function example() {
const data = await fetchData(); // SyntaxError
}
Converting Promises to Async/Await
// Using .then
function getUserPosts() {
return fetchUser(1)
.then((user) => fetchPosts(user.id))
.then((posts) => posts)
.catch((error) => console.error(error));
}
// ✅ Using async/await (cleaner!)
async function getUserPosts() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
return posts;
} catch (error) {
console.error(error);
}
}
Top-Level Await (ES2022)
// In modules, you can use await at the top level
// (Outside of async functions)
// module.js
const data = await fetchData();
export default data;
// Or
export const users = await fetch('/api/users').then(r => r.json());
Error Handling
Proper error handling is crucial for robust async code.
Try/Catch with Async/Await
// Basic try/catch
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
throw error; // Re-throw if needed
}
}
// Multiple operations
async function getData() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (error) {
console.error('Error in getData:', error);
return null;
}
}
// With finally
async function updateUser(id, data) {
try {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
});
return await response.json();
} catch (error) {
console.error('Update failed:', error);
throw error;
} finally {
console.log('Update attempt completed');
}
}
Catch with Promises
// Using .catch()
fetchUser(1)
.then((user) => fetchPosts(user.id))
.then((posts) => console.log(posts))
.catch((error) => {
console.error('Error:', error);
});
// Multiple catch blocks
fetchUser(1)
.then((user) => {
return fetchPosts(user.id);
})
.catch((error) => {
console.error('User fetch failed:', error);
return []; // Return default value
})
.then((posts) => {
console.log('Posts:', posts);
})
.catch((error) => {
console.error('Posts fetch failed:', error);
});
Error Handling Patterns
// Wrap individual awaits for specific handling
async function fetchData() {
let user, posts;
try {
user = await fetchUser(1);
} catch (error) {
console.error('User fetch failed:', error);
user = { id: 1, name: 'Default User' };
}
try {
posts = await fetchPosts(user.id);
} catch (error) {
console.error('Posts fetch failed:', error);
posts = [];
}
return { user, posts };
}
// Custom error handling function
async function handleAsync(promise) {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error, null];
}
}
// Usage
const [error, user] = await handleAsync(fetchUser(1));
if (error) {
console.error(error);
} else {
console.log(user);
}
HTTP Error Handling
// fetch() doesn't reject on HTTP errors (404, 500, etc.)
// You need to check response.ok
async function fetchWithErrorHandling(url) {
try {
const response = await fetch(url);
// Check if request was successful
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
throw error;
}
}
// Reusable fetch wrapper
async function fetchJSON(url, options = {}) {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
// Usage
try {
const user = await fetchJSON('/api/user/1');
console.log(user);
} catch (error) {
console.error('Failed:', error.message);
}
Promise Methods
Powerful methods for handling multiple Promises.
Promise.all()
// Wait for ALL promises to resolve
// Rejects if ANY promise rejects
const promise1 = fetch('/api/users');
const promise2 = fetch('/api/posts');
const promise3 = fetch('/api/comments');
Promise.all([promise1, promise2, promise3])
.then(([users, posts, comments]) => {
console.log('All data loaded');
})
.catch((error) => {
console.error('One failed:', error);
});
// With async/await
async function loadAllData() {
try {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { users, posts, comments };
} catch (error) {
console.error('Failed to load data:', error);
}
}
// Practical example: parallel requests
async function getUserData(userId) {
const [profile, posts, followers] = await Promise.all([
fetchProfile(userId),
fetchPosts(userId),
fetchFollowers(userId)
]);
return { profile, posts, followers };
}
Promise.allSettled()
// Wait for ALL promises to complete (resolve OR reject)
// Never rejects, always resolves with array of results
const promises = [
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/invalid') // This will fail
];
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index} succeeded:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason);
}
});
// Result format:
// [
// { status: 'fulfilled', value: ... },
// { status: 'fulfilled', value: ... },
// { status: 'rejected', reason: Error }
// ]
// Practical example: try multiple sources
async function fetchFromMultipleSources(id) {
const results = await Promise.allSettled([
fetchFromAPI1(id),
fetchFromAPI2(id),
fetchFromCache(id)
]);
// Return first successful result
const success = results.find(r => r.status === 'fulfilled');
return success ? success.value : null;
}
Promise.race()
// Resolves/rejects as soon as ANY promise resolves/rejects
const promise1 = new Promise(resolve => setTimeout(() => resolve('slow'), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve('fast'), 100));
const result = await Promise.race([promise1, promise2]);
console.log(result); // 'fast'
// Timeout pattern
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
}
async function fetchWithTimeout(url, ms = 5000) {
try {
const response = await Promise.race([
fetch(url),
timeout(ms)
]);
return response;
} catch (error) {
console.error('Request timed out or failed:', error);
throw error;
}
}
// Usage
try {
const data = await fetchWithTimeout('/api/slow-endpoint', 3000);
} catch (error) {
console.log('Request took too long!');
}
Promise.any()
// Resolves as soon as ANY promise fulfills
// Rejects only if ALL promises reject
const promises = [
fetch('/api/backup1'),
fetch('/api/backup2'),
fetch('/api/backup3')
];
try {
const first = await Promise.any(promises);
console.log('First successful response:', first);
} catch (error) {
console.error('All requests failed:', error);
}
// Practical: try multiple mirrors
async function fetchFromMirrors(path) {
const mirrors = [
`https://cdn1.example.com${path}`,
`https://cdn2.example.com${path}`,
`https://cdn3.example.com${path}`
];
try {
const response = await Promise.any(
mirrors.map(url => fetch(url))
);
return response;
} catch (error) {
throw new Error('All CDN mirrors failed');
}
}
Comparison Table
// Promise.all() - Wait for all, fail if any fails
// Promise.allSettled() - Wait for all, never fails
// Promise.race() - First to finish (resolve or reject)
// Promise.any() - First to succeed, fail if all fail
// Examples
await Promise.all([p1, p2, p3]); // [v1, v2, v3] or throws
await Promise.allSettled([p1, p2, p3]);// [{...}, {...}, {...}]
await Promise.race([p1, p2, p3]); // v1 (fastest)
await Promise.any([p1, p2, p3]); // v1 (first success)
Fetch API
Modern way to make HTTP requests using Promises.
Basic GET Request
// Simple GET
async function getUsers() {
const response = await fetch('/api/users');
const users = await response.json();
return users;
}
// With error handling
async function getUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Failed to fetch users:', error);
throw error;
}
}
// Check response type
async function fetchData(url) {
const response = await fetch(url);
const contentType = response.headers.get('content-type');
if (contentType.includes('application/json')) {
return response.json();
} else if (contentType.includes('text/')) {
return response.text();
} else {
return response.blob();
}
}
POST Request
// POST with JSON
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
}
// Usage
const newUser = await createUser({
name: 'Jane Doe',
email: '[email protected]'
});
Other HTTP Methods
// PUT (update)
async function updateUser(id, userData) {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return response.json();
}
// PATCH (partial update)
async function patchUser(id, updates) {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
return response.json();
}
// DELETE
async function deleteUser(id) {
const response = await fetch(`/api/users/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete user');
}
return true;
}
Request with Headers & Auth
// With authentication
async function fetchProtectedData() {
const response = await fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.json();
}
// Custom headers
async function fetchWithHeaders(url) {
const response = await fetch(url, {
headers: {
'X-Custom-Header': 'value',
'Accept': 'application/json',
'Authorization': 'Bearer token123'
}
});
return response.json();
}
// Reusable fetch with defaults
const api = {
baseURL: '/api',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`
},
async get(endpoint) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
headers: this.headers
});
return response.json();
},
async post(endpoint, data) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify(data)
});
return response.json();
}
};
// Usage
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John' });
Handling Different Response Types
// JSON response
const data = await response.json();
// Text response
const text = await response.text();
// Blob (for images, files)
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
// ArrayBuffer (for binary data)
const buffer = await response.arrayBuffer();
// FormData
const formData = await response.formData();
// Clone response (can only read body once)
const response = await fetch('/api/data');
const clone = response.clone();
const data1 = await response.json();
const data2 = await clone.json();
Common Patterns
Practical async patterns you'll use frequently.
Sequential vs Parallel Execution
// ❌ Sequential (slow) - waits for each one
async function getDataSequential() {
const users = await fetchUsers(); // Wait
const posts = await fetchPosts(); // Wait
const comments = await fetchComments(); // Wait
// Total time: sum of all requests
return { users, posts, comments };
}
// ✅ Parallel (fast) - all at once
async function getDataParallel() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
// Total time: longest single request
return { users, posts, comments };
}
// Mixed: sequential with dependencies
async function getUserWithPosts(userId) {
const user = await fetchUser(userId); // Must get user first
const posts = await fetchPosts(user.id); // Then get their posts
return { user, posts };
}
// Mixed: some parallel, some sequential
async function loadDashboard(userId) {
// First, get user (needed for other requests)
const user = await fetchUser(userId);
// Then fetch everything else in parallel
const [posts, friends, notifications] = await Promise.all([
fetchPosts(user.id),
fetchFriends(user.id),
fetchNotifications(user.id)
]);
return { user, posts, friends, notifications };
}
Retry Logic
// Retry on failure
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (response.ok) {
return response.json();
}
} catch (error) {
console.log(`Attempt ${i + 1} failed`);
if (i === maxRetries - 1) throw error;
// Wait before retry (exponential backoff)
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
}
}
}
// Retry with exponential backoff
async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = baseDelay * Math.pow(2, i);
console.log(`Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const data = await retryWithBackoff(
() => fetch('/api/unreliable').then(r => r.json()),
3,
1000
);
Debouncing Async Operations
// Search with debounce
let searchTimeout;
async function searchWithDebounce(query) {
clearTimeout(searchTimeout);
return new Promise((resolve) => {
searchTimeout = setTimeout(async () => {
const results = await fetch(`/api/search?q=${query}`)
.then(r => r.json());
resolve(results);
}, 300); // Wait 300ms after last keystroke
});
}
// Usage with input
input.addEventListener('input', async (e) => {
const results = await searchWithDebounce(e.target.value);
displayResults(results);
});
// Reusable debounce function
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
return new Promise((resolve) => {
timeoutId = setTimeout(async () => {
resolve(await fn(...args));
}, delay);
});
};
}
// Usage
const debouncedSearch = debounce(searchAPI, 300);
const results = await debouncedSearch('query');
Loading States
// Manage loading, data, and error states
async function loadUserData(userId) {
const state = {
loading: true,
data: null,
error: null
};
try {
state.data = await fetchUser(userId);
} catch (error) {
state.error = error.message;
} finally {
state.loading = false;
}
return state;
}
// React-style pattern
async function useAsync(asyncFn) {
let loading = true;
let data = null;
let error = null;
try {
data = await asyncFn();
} catch (err) {
error = err;
} finally {
loading = false;
}
return { loading, data, error };
}
// Usage
const { loading, data, error } = await useAsync(
() => fetch('/api/users').then(r => r.json())
);
if (loading) console.log('Loading...');
if (error) console.error('Error:', error);
if (data) console.log('Data:', data);
Polling
// Poll for updates
async function poll(fn, interval = 2000, maxAttempts = 10) {
let attempts = 0;
while (attempts < maxAttempts) {
try {
const result = await fn();
if (result.complete) {
return result;
}
} catch (error) {
console.error('Poll failed:', error);
}
await new Promise(resolve => setTimeout(resolve, interval));
attempts++;
}
throw new Error('Max polling attempts reached');
}
// Usage: check job status
async function waitForJobCompletion(jobId) {
return poll(
async () => {
const response = await fetch(`/api/jobs/${jobId}`);
return response.json();
},
2000,
30
);
}
// Poll until condition met
async function pollUntil(fn, condition, interval = 1000) {
while (true) {
const result = await fn();
if (condition(result)) {
return result;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
}
// Usage
const data = await pollUntil(
() => fetch('/api/status').then(r => r.json()),
(status) => status.ready === true,
2000
);
Concurrent Limit
// Process array with concurrency limit
async function processBatch(items, batchSize = 3) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}
// Usage
const urls = ['url1', 'url2', 'url3', ...];
const data = await processBatch(
urls.map(url => () => fetch(url).then(r => r.json())),
3 // Process 3 at a time
);
// More sophisticated concurrency control
async function pLimit(concurrency) {
const queue = [];
let activeCount = 0;
const next = () => {
activeCount--;
if (queue.length > 0) {
queue.shift()();
}
};
const run = async (fn) => {
activeCount++;
try {
return await fn();
} finally {
next();
}
};
const enqueue = (fn) => {
return new Promise((resolve) => {
const run = () => {
resolve(run(fn));
};
if (activeCount < concurrency) {
run();
} else {
queue.push(run);
}
});
};
return enqueue;
}
// Usage
const limit = pLimit(3);
const urls = [...];
const results = await Promise.all(
urls.map(url => limit(() => fetch(url).then(r => r.json())))
);
Best Practices
Always Handle Errors
// ❌ Bad: unhandled promise rejection
async function bad() {
const data = await fetch('/api/data').then(r => r.json());
return data;
}
// ✅ Good: proper error handling
async function good() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
}
Avoid Mixing Promises and Async/Await
// ❌ Bad: mixing styles is confusing
async function mixed() {
return fetchData()
.then(data => processData(data))
.then(result => result);
}
// ✅ Good: consistent async/await
async function consistent() {
const data = await fetchData();
const result = await processData(data);
return result;
}
Don't Await Unnecessarily
// ❌ Bad: unnecessary await
async function bad() {
return await fetchData();
}
// ✅ Good: just return the promise
async function good() {
return fetchData();
}
// ❌ Bad: awaiting when you don't need the value
async function bad() {
await doSomething();
await doSomethingElse();
// If these don't depend on each other...
}
// ✅ Good: parallel execution
async function good() {
await Promise.all([
doSomething(),
doSomethingElse()
]);
}
Use Promise.all for Parallel Requests
// ❌ Bad: sequential (slow)
async function loadData() {
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();
return { users, posts, comments };
}
// ✅ Good: parallel (fast)
async function loadData() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
}
Clean Up Resources
// Use finally for cleanup
async function uploadFile(file) {
let uploadInProgress = true;
showLoader();
try {
const result = await upload(file);
return result;
} catch (error) {
handleError(error);
throw error;
} finally {
uploadInProgress = false;
hideLoader(); // Always cleanup
}
}
Add Timeouts to Prevent Hanging
// Add timeout to any async operation
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
]);
}
// Usage
try {
const data = await withTimeout(
fetch('/api/slow'),
5000
);
} catch (error) {
console.error('Request timed out or failed');
}
Common Mistakes to Avoid
// ❌ Mistake 1: Forgetting async keyword
function bad() {
await fetchData(); // SyntaxError!
}
// ❌ Mistake 2: Not returning in Promise chain
async function bad() {
fetchData()
.then(data => {
processData(data); // Not returned!
});
}
// ❌ Mistake 3: Using await in loops unnecessarily
async function bad(items) {
for (const item of items) {
await processItem(item); // One at a time (slow)
}
}
// ✅ Better: process in parallel
async function good(items) {
await Promise.all(items.map(item => processItem(item)));
}
// ❌ Mistake 4: Not checking response.ok with fetch
async function bad() {
const data = await fetch('/api/data').then(r => r.json());
// Doesn't handle 404, 500, etc!
}
// ✅ Good: check response
async function good() {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('HTTP error');
return response.json();
}