What is Async/Await?
Async/await is syntactic sugar over Promises, making asynchronous code look and behave more like synchronous code. It provides a cleaner way to work with Promises.
π€ async Keyword
// async function always returns a promise
async function getData() {
return 'Hello';
}
getData().then(value => console.log(value)); // 'Hello'
// Same as
function getData() {
return Promise.resolve('Hello');
}
// Return promise directly
async function fetchUser() {
return fetch('/api/user').then(r => r.json());
}
// Throw error = rejected promise
async function failingFunction() {
throw new Error('Oops');
}
failingFunction().catch(error => {
console.error(error.message); // 'Oops'
});
// async arrow function
const getData = async () => {
return 'Hello';
};
// async method
class API {
async getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
}
βΈοΈ await Keyword
// await pauses execution until promise resolves
async function getData() {
let response = await fetch('/api/data');
let data = await response.json();
return data;
}
// Without await (promise chain)
function getData() {
return fetch('/api/data')
.then(response => response.json())
.then(data => data);
}
// Can only use await inside async function
async function example() {
let result = await somePromise(); // OK
}
// await somePromise(); // SyntaxError: outside async function
// Top-level await (ES2022, in modules only)
// script.js (type="module")
let data = await fetch('/api/data').then(r => r.json());
console.log(data);
// await unwraps promise
async function example() {
let promise = Promise.resolve(42);
let value = await promise;
console.log(value); // 42 (not a promise)
console.log(typeof value); // 'number'
}
π‘οΈ Error Handling
try-catch
// Catch errors with try-catch
async function fetchData() {
try {
let response = await fetch('/api/data');
let data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch:', error);
return null;
}
}
// Multiple operations
async function loadDashboard() {
try {
let user = await fetchUser();
let posts = await fetchPosts(user.id);
let stats = await fetchStats();
return { user, posts, stats };
} catch (error) {
console.error('Dashboard load failed:', error);
throw error; // Re-throw if needed
}
}
// Finally block
async function processData() {
let file;
try {
file = await openFile('data.txt');
let data = await readFile(file);
return processedData(data);
} catch (error) {
console.error('Processing failed:', error);
return null;
} finally {
if (file) {
await closeFile(file); // Cleanup
}
}
}
catch() on await
// Catch specific operation
async function example() {
let data = await fetchData().catch(error => {
console.error('Fetch failed:', error);
return defaultData; // Fallback
});
console.log(data); // Either fetched or default
}
// Mix try-catch and .catch()
async function hybrid() {
try {
let user = await fetchUser();
// Handle this error specifically
let posts = await fetchPosts(user.id).catch(error => {
console.warn('No posts:', error);
return []; // Empty array fallback
});
return { user, posts };
} catch (error) {
console.error('Critical error:', error);
}
}
Error Propagation
// Errors bubble up
async function level3() {
throw new Error('Level 3 error');
}
async function level2() {
await level3(); // Error propagates
}
async function level1() {
try {
await level2();
} catch (error) {
console.error('Caught at level 1:', error.message);
}
}
level1(); // 'Caught at level 1: Level 3 error'
// Without try-catch, becomes unhandled rejection
async function unhandled() {
await Promise.reject('Error'); // Unhandled
}
unhandled(); // UnhandledPromiseRejectionWarning
// Always handle at some level
async function handled() {
try {
await unhandled();
} catch (error) {
console.error('Handled:', error);
}
}
π Patterns and Techniques
Sequential Operations
// Operations run one after another
async function sequential() {
let user = await fetchUser(); // Wait
let posts = await fetchPosts(); // Then wait
let comments = await fetchComments(); // Then wait
return { user, posts, comments };
}
// Total time = sum of all operations
// Process array sequentially
async function processSequentially(items) {
let results = [];
for (let item of items) {
let result = await processItem(item);
results.push(result);
}
return results;
}
// for-of with await
async function example() {
let urls = ['/api/1', '/api/2', '/api/3'];
for (let url of urls) {
let response = await fetch(url);
let data = await response.json();
console.log(data);
}
}
Parallel Operations
// Run operations in parallel (faster)
async function parallel() {
// Start all promises
let userPromise = fetchUser();
let postsPromise = fetchPosts();
let commentsPromise = fetchComments();
// Wait for all to complete
let user = await userPromise;
let posts = await postsPromise;
let comments = await commentsPromise;
return { user, posts, comments };
}
// Or use Promise.all
async function parallelAll() {
let [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
return { user, posts, comments };
}
// Process array in parallel
async function processParallel(items) {
let promises = items.map(item => processItem(item));
let results = await Promise.all(promises);
return results;
}
// Real example
async function loadDashboard() {
let [users, posts, stats] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/stats').then(r => r.json())
]);
return { users, posts, stats };
}
Conditional Operations
// Conditional await
async function example(shouldFetch) {
if (shouldFetch) {
let data = await fetchData();
return data;
}
return cachedData;
}
// Early return
async function getUser(id) {
if (!id) {
return null;
}
let user = await fetchUser(id);
if (!user.isActive) {
return null;
}
let profile = await fetchProfile(user.id);
return { ...user, profile };
}
// Retry logic
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
let response = await fetch(url);
if (response.ok) {
return await response.json();
}
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
Timeout Pattern
// Add timeout to async operation
async function withTimeout(promise, ms) {
let timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([promise, timeout]);
}
// Usage
async function example() {
try {
let data = await withTimeout(fetchData(), 5000);
console.log(data);
} catch (error) {
if (error.message === 'Timeout') {
console.error('Request timed out');
} else {
console.error('Request failed:', error);
}
}
}
// AbortController for fetch timeout
async function fetchWithTimeout(url, ms) {
let controller = new AbortController();
let timeout = setTimeout(() => controller.abort(), ms);
try {
let response = await fetch(url, { signal: controller.signal });
clearTimeout(timeout);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timed out');
}
throw error;
}
}
π‘ Practical Examples
API Client
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async get(endpoint) {
let response = await fetch(`${this.baseURL}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
async post(endpoint, data) {
let response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
async delete(endpoint) {
let response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
}
// Usage
let api = new APIClient('https://api.example.com');
async function loadUsers() {
try {
let users = await api.get('/users');
console.log(users);
} catch (error) {
console.error('Failed to load users:', error);
}
}
Form Submission
async function handleSubmit(event) {
event.preventDefault();
let form = event.target;
let submitBtn = form.querySelector('button[type="submit"]');
let errorDiv = form.querySelector('.error');
// Disable button
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
errorDiv.textContent = '';
try {
let formData = new FormData(form);
let data = Object.fromEntries(formData);
let response = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
let error = await response.json();
throw new Error(error.message);
}
let result = await response.json();
console.log('Success:', result);
form.reset();
showNotification('Submitted successfully!');
} catch (error) {
errorDiv.textContent = error.message;
console.error('Submission failed:', error);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Submit';
}
}
document.querySelector('form').addEventListener('submit', handleSubmit);
Data Loading with Cache
class DataLoader {
constructor() {
this.cache = new Map();
}
async load(key, fetcher, ttl = 60000) {
// Check cache
if (this.cache.has(key)) {
let { data, timestamp } = this.cache.get(key);
if (Date.now() - timestamp < ttl) {
return data;
}
}
// Fetch new data
let data = await fetcher();
// Store in cache
this.cache.set(key, {
data,
timestamp: Date.now()
});
return data;
}
clear(key) {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
}
// Usage
let loader = new DataLoader();
async function getUser(id) {
return loader.load(
`user:${id}`,
() => fetch(`/api/users/${id}`).then(r => r.json()),
30000 // 30 second TTL
);
}
// First call fetches
let user1 = await getUser(1);
// Second call uses cache
let user2 = await getUser(1);
Batch Operations
async function processBatch(items, batchSize = 10) {
let results = [];
for (let i = 0; i < items.length; i += batchSize) {
let batch = items.slice(i, i + batchSize);
console.log(`Processing batch ${i / batchSize + 1}...`);
let batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}
// Usage
let items = Array.from({ length: 100 }, (_, i) => i);
async function processAll() {
try {
let results = await processBatch(items, 10);
console.log(`Processed ${results.length} items`);
} catch (error) {
console.error('Batch processing failed:', error);
}
}
processAll();
Concurrent Limit
async function mapWithLimit(items, limit, fn) {
let results = [];
let executing = [];
for (let [index, item] of items.entries()) {
let promise = Promise.resolve().then(() => fn(item, index));
results.push(promise);
if (limit <= items.length) {
let 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: max 3 concurrent requests
let urls = Array.from({ length: 20 }, (_, i) => `/api/item/${i}`);
async function loadAll() {
let results = await mapWithLimit(urls, 3, async (url) => {
let response = await fetch(url);
return response.json();
});
console.log('Loaded:', results.length);
}
loadAll();
Polling
async function poll(fn, interval = 1000, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
try {
let result = await fn();
if (result) {
return result;
}
} catch (error) {
console.warn(`Attempt ${i + 1} failed:`, error);
}
if (i < maxAttempts - 1) {
await new Promise(resolve => setTimeout(resolve, interval));
}
}
throw new Error('Max attempts reached');
}
// Usage: wait for job completion
async function waitForJob(jobId) {
return poll(
async () => {
let response = await fetch(`/api/jobs/${jobId}`);
let job = await response.json();
if (job.status === 'completed') {
return job;
} else if (job.status === 'failed') {
throw new Error('Job failed');
}
return null; // Keep polling
},
2000, // Check every 2 seconds
30 // Max 30 attempts (1 minute)
);
}
try {
let job = await waitForJob('job-123');
console.log('Job completed:', job);
} catch (error) {
console.error('Job polling failed:', error);
}
Async Iteration
// Async generators
async function* fetchPages(url) {
let page = 1;
let hasMore = true;
while (hasMore) {
let response = await fetch(`${url}?page=${page}`);
let data = await response.json();
yield data.items;
hasMore = data.hasMore;
page++;
}
}
// Usage with for-await-of
async function loadAllPages() {
for await (let items of fetchPages('/api/items')) {
console.log('Loaded page:', items.length);
processItems(items);
}
}
// Async iterable class
class AsyncQueue {
constructor() {
this.queue = [];
this.waiting = [];
}
push(item) {
if (this.waiting.length > 0) {
let resolve = this.waiting.shift();
resolve(item);
} else {
this.queue.push(item);
}
}
async next() {
if (this.queue.length > 0) {
return this.queue.shift();
}
return new Promise(resolve => {
this.waiting.push(resolve);
});
}
async *[Symbol.asyncIterator]() {
while (true) {
yield await this.next();
}
}
}
// Usage
let queue = new AsyncQueue();
async function consumer() {
for await (let item of queue) {
console.log('Processing:', item);
if (item === 'END') break;
}
}
consumer();
queue.push('Item 1');
queue.push('Item 2');
queue.push('END');
β οΈ Common Mistakes
// Forgetting await
async function bad() {
let data = fetchData(); // Returns promise, not data!
console.log(data); // Promise { }
}
async function good() {
let data = await fetchData();
console.log(data); // Actual data
}
// Sequential when should be parallel
async function slow() {
let user = await fetchUser(); // Wait
let posts = await fetchPosts(); // Wait
let stats = await fetchStats(); // Wait
}
async function fast() {
let [user, posts, stats] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchStats()
]);
}
// Forgetting error handling
async function risky() {
let data = await fetchData(); // Might throw
return data;
}
risky(); // Unhandled promise rejection!
async function safe() {
try {
let data = await fetchData();
return data;
} catch (error) {
console.error(error);
}
}
// Using await in loop when parallel is better
async function inefficient(ids) {
let users = [];
for (let id of ids) {
let user = await fetchUser(id); // One at a time
users.push(user);
}
return users;
}
async function efficient(ids) {
return Promise.all(ids.map(id => fetchUser(id)));
}
// Async forEach doesn't work as expected
async function broken(items) {
items.forEach(async item => {
await processItem(item); // Doesn't wait!
});
console.log('Done?'); // Runs immediately
}
async function fixed(items) {
for (let item of items) {
await processItem(item); // Properly waits
}
console.log('Done!'); // Runs after all
}
π― Key Takeaways
- async: Function always returns Promise, enables await usage
- await: Pauses execution until Promise resolves, unwraps value
- try-catch: Handle errors from await like synchronous code
- Sequential: await in sequence = operations run one by one
- Parallel: Start promises first, then await = faster execution
- Promise.all: Combine with async/await for parallel operations
- Error Handling: Always use try-catch or .catch() to handle rejections
- Cleaner Code: Looks synchronous, easier to read than promise chains