Getting started
Introduction
Async/await is modern JavaScript syntax for working with Promises. It makes asynchronous code look and behave more like synchronous code.
Browser Support: Baseline Widely Available (April 2017+)
Basic Async Function
async function fetchUser() {
const response = await fetch("/api/user");
const data = await response.json();
return data;
}
// Returns a Promise
fetchUser().then((user) => console.log(user));
async functions always return a Promise. await pauses execution until the Promise resolves.
Async Arrow Functions
const fetchUser = async () => {
const response = await fetch("/api/user");
return response.json();
};
// One-liner
const getUser = async (id) => await fetch(`/api/user/${id}`);
Arrow functions can be async too.
Quick Example
async function main() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
console.log(posts);
} catch (error) {
console.error("Error:", error);
}
}
main();
Basic Syntax
Async Function Declaration
async function myFunction() {
return "Hello";
}
// Equivalent to:
function myFunction() {
return Promise.resolve("Hello");
}
Async Class Methods
class UserService {
async getUser(id) {
const response = await fetch(`/api/user/${id}`);
return response.json();
}
async updateUser(id, data) {
await fetch(`/api/user/${id}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
}
Await Expression
// Wait for Promise to resolve
const result = await promise;
// Works with any thenable
const value = await Promise.resolve(42);
// Wait for function call
const data = await fetchData();
await only works inside async functions (or at top level in modules).
Top-Level Await (ES2022)
// In ES modules (.mjs or type: "module")
const response = await fetch("/api/config");
const config = await response.json();
export default config;
Use in module scope without wrapping in async function.
Error Handling
Try...Catch
async function fetchData() {
try {
const response = await fetch("/api/data");
const data = await response.json();
return data;
} catch (error) {
console.error("Fetch failed:", error);
throw error; // Re-throw or handle
}
}
Standard error handling pattern.
Multiple Try Blocks
async function processData() {
let user, posts;
try {
user = await fetchUser();
} catch (error) {
console.error("User fetch failed:", error);
return null;
}
try {
posts = await fetchPosts(user.id);
} catch (error) {
console.error("Posts fetch failed:", error);
posts = []; // Fallback
}
return { user, posts };
}
Handle errors at different levels.
Return vs Return Await
// ❌ Wrong: Error not caught
async function wrong() {
try {
return fetch("/api/data"); // Returns immediately
} catch (error) {
// Never reached if fetch fails
}
}
// ✅ Correct: Error caught
async function correct() {
try {
return await fetch("/api/data"); // Waits for Promise
} catch (error) {
console.error(error); // Catches fetch errors
}
}
Use return await in try blocks to catch errors.
Error Propagation
async function inner() {
throw new Error("Inner error");
}
async function middle() {
await inner(); // Error propagates up
}
async function outer() {
try {
await middle();
} catch (error) {
console.error("Caught:", error.message);
}
}
Errors bubble up through async call stack.
Concurrency Patterns
Promise.all() - Parallel
async function fetchAll() {
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 };
}
Run multiple Promises in parallel. Fails fast if any Promise rejects.
Promise.allSettled()
async function fetchAllSafe() {
const results = await Promise.allSettled([
fetch("/api/users"),
fetch("/api/posts"),
fetch("/api/comments"),
]);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Request ${index} succeeded:`, result.value);
} else {
console.error(`Request ${index} failed:`, result.reason);
}
});
}
Wait for all Promises, regardless of success or failure.
Promise.race()
async function fetchWithTimeout(url, timeout = 5000) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeout),
);
const response = await Promise.race([fetch(url), timeoutPromise]);
return response;
}
Returns first settled Promise (success or failure).
Promise.any()
async function fetchFromMirrors() {
const mirrors = [
"https://cdn1.example.com/data",
"https://cdn2.example.com/data",
"https://cdn3.example.com/data",
];
const response = await Promise.any(mirrors.map((url) => fetch(url)));
return response.json();
}
Returns first successful Promise. Rejects only if all fail.
Sequential vs Parallel
// ❌ Sequential (slower): 3 seconds total
async function sequential() {
const user = await fetchUser(); // 1s
const posts = await fetchPosts(); // 1s
const comments = await fetchComments(); // 1s
return { user, posts, comments };
}
// ✅ Parallel (faster): 1 second total
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(), // All start immediately
fetchPosts(),
fetchComments(),
]);
return { user, posts, comments };
}
Use parallel execution when operations are independent.
Controlled Parallelism
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;
}
Process items in batches to limit concurrency.
Common Patterns
Async IIFE
// Execute async code immediately
(async () => {
const data = await fetchData();
console.log(data);
})();
// Or with error handling
(async () => {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
})();
Immediately Invoked Function Expression for top-level async code.
Fetching Data
async function fetchUserData(userId) {
const response = await fetch(`/api/user/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// With headers
async function postData(url, data) {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return response.json();
}
Multiple Awaits
async function getUserPosts(userId) {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts);
return {
user,
posts: posts.map((post) => ({
...post,
comments: comments.filter((c) => c.postId === post.id),
})),
};
}
Chain dependent async operations.
Conditional Async
async function getData(useCache = true) {
if (useCache) {
const cached = await getFromCache();
if (cached) return cached;
}
const fresh = await fetchFromAPI();
if (useCache) {
await saveToCache(fresh);
}
return fresh;
}
Retry Logic
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await fetch(url);
} catch (error) {
if (i === retries - 1) throw error;
await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
}
}
}
Retry failed operations with exponential backoff.
Async Array Methods
// Map with await
const users = await Promise.all(
userIds.map(async (id) => {
return await fetchUser(id);
}),
);
// Filter with await
const activeUsers = await Promise.all(
users.map(async (user) => ({
user,
isActive: await checkUserActive(user.id),
})),
).then((results) => results.filter((r) => r.isActive).map((r) => r.user));
// Reduce with await
const total = await items.reduce(async (accPromise, item) => {
const acc = await accPromise;
const value = await getValue(item);
return acc + value;
}, Promise.resolve(0));
TypeScript
Async Return Types
// Explicit Promise type
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/user/${id}`);
return response.json();
}
// Type inference works too
async function getUser(id: string) {
// Return type inferred as Promise<User>
const user: User = await fetchUser(id);
return user;
}
Promise Generic Types
// Promise with generic type
const promise: Promise<number> = Promise.resolve(42);
// Await infers type
const value: number = await promise;
// Array of Promises
const promises: Promise<string>[] = [
fetch("/a").then((r) => r.text()),
fetch("/b").then((r) => r.text()),
];
const results: string[] = await Promise.all(promises);
Async Function Types
// Function type
type AsyncFunc = () => Promise<void>;
const myFunc: AsyncFunc = async () => {
await doSomething();
};
// Generic async function
type FetchFunc<T> = (id: string) => Promise<T>;
const fetchUser: FetchFunc<User> = async (id) => {
const response = await fetch(`/api/user/${id}`);
return response.json();
};
Type Guards with Async
interface User {
id: string;
name: string;
}
interface Admin extends User {
role: 'admin';
}
async function isAdmin(user: User): Promise<user is Admin> {
const roles = await fetchUserRoles(user.id);
return roles.includes('admin');
}
// Usage
const user = await fetchUser('123');
if (await isAdmin(user)) {
// user is typed as Admin here
console.log(user.role);
}
Common Pitfalls
Floating Promises
// ❌ Wrong: Promise ignored
async function save() {
saveToDatabase(data); // No await!
return "Saved";
}
// ✅ Correct: Wait for completion
async function save() {
await saveToDatabase(data);
return "Saved";
}
// ✅ Or explicitly ignore
async function save() {
void saveToDatabase(data); // Intentional
return "Saved";
}
Always await Promises or explicitly void them.
Sequential Loops
// ❌ Slow: Sequential execution
async function processItems(items) {
for (const item of items) {
await processItem(item); // Waits for each
}
}
// ✅ Fast: Parallel execution
async function processItems(items) {
await Promise.all(items.map((item) => processItem(item)));
}
Don't await inside loops unless order matters.
forEach with Async
// ❌ Wrong: Doesn't wait
async function processAll(items) {
items.forEach(async (item) => {
await processItem(item); // forEach doesn't await
});
console.log("Done"); // Runs immediately!
}
// ✅ Correct: Use for...of
async function processAll(items) {
for (const item of items) {
await processItem(item);
}
console.log("Done"); // Runs after all complete
}
// ✅ Or Promise.all
async function processAll(items) {
await Promise.all(items.map((item) => processItem(item)));
console.log("Done");
}
forEach doesn't work with async/await.
Missing Return Await
// ❌ Wrong: Error escapes try block
async function getData() {
try {
return fetchData(); // Returns Promise immediately
} catch (error) {
// Never catches fetchData errors
}
}
// ✅ Correct
async function getData() {
try {
return await fetchData();
} catch (error) {
console.error(error);
}
}
Mixing Callbacks and Promises
// ❌ Wrong: Mixing paradigms
async function readFile(path) {
fs.readFile(path, (err, data) => {
if (err) throw err; // Doesn't work with async!
return data;
});
}
// ✅ Correct: Promisify or use Promise API
async function readFile(path) {
return fs.promises.readFile(path);
}
// ✅ Or wrap callback
async function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
Best Practices
Always Handle Errors
// ✅ With try...catch
async function getData() {
try {
return await fetch("/api/data");
} catch (error) {
console.error("Fetch failed:", error);
throw error;
}
}
// ✅ Or with .catch()
async function getData() {
return fetch("/api/data").catch((error) => {
console.error("Fetch failed:", error);
throw error;
});
}
// ✅ Or at call site
getData().catch((error) => {
console.error("Error:", error);
});
Prefer Parallel Execution
// ✅ Good: Parallel for independent operations
async function fetchData() {
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
return { users, posts };
}
// ✅ Good: Sequential for dependent operations
async function getUserPosts(userId) {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id); // Needs user.id
return posts;
}
Use Async/Await Over Chains
// ❌ Less readable
function getUserData(userId) {
return fetchUser(userId)
.then((user) => fetchPosts(user.id))
.then((posts) => fetchComments(posts))
.then((comments) => ({ posts, comments }))
.catch((error) => console.error(error));
}
// ✅ More readable
async function getUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts);
return { posts, comments };
} catch (error) {
console.error(error);
}
}
Handle Errors at Appropriate Level
// ✅ Low-level function throws
async function fetchUser(id) {
const response = await fetch(`/api/user/${id}`);
if (!response.ok) throw new Error("Fetch failed");
return response.json();
}
// ✅ High-level function handles
async function displayUser(id) {
try {
const user = await fetchUser(id);
renderUser(user);
} catch (error) {
showError("Could not load user");
logError(error);
}
}
Avoid Memory Leaks
// ✅ Clean up in finally
async function processData() {
const connection = await openConnection();
try {
return await connection.query("SELECT * FROM users");
} finally {
await connection.close(); // Always runs
}
}
// ✅ Use AbortController for fetch
async function fetchWithCancel(url, signal) {
try {
const response = await fetch(url, { signal });
return response.json();
} catch (error) {
if (error.name === "AbortError") {
console.log("Fetch cancelled");
}
throw error;
}
}
Document Async Dependencies
/**
* Fetches user data and their posts
* @param userId - User ID to fetch
* @returns Promise resolving to user with posts
* @throws {Error} If user not found or network fails
*/
async function getUserWithPosts(userId: string): Promise<UserWithPosts> {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return { ...user, posts };
}
Examples
REST API Client
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
async get(endpoint) {
return this.request(endpoint);
}
async post(endpoint, data) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
async put(endpoint, data) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
async delete(endpoint) {
return this.request(endpoint, {
method: "DELETE",
});
}
}
// Usage
const client = new APIClient("https://api.example.com");
const users = await client.get("/users");
await client.post("/users", { name: "John" });
Rate Limiter
class RateLimiter {
constructor(maxConcurrent = 3) {
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);
const urls = [...Array(10)].map((_, i) => `/api/data/${i}`);
const results = await Promise.all(
urls.map((url) => limiter.run(() => fetch(url))),
);
Cache with TTL
class AsyncCache {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}
async get(key, fetcher) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.value;
}
const value = await fetcher();
this.cache.set(key, {
value,
timestamp: Date.now(),
});
return value;
}
clear() {
this.cache.clear();
}
}
// Usage
const cache = new AsyncCache(30000); // 30 second TTL
async function getUser(id) {
return cache.get(`user:${id}`, () =>
fetch(`/api/user/${id}`).then((r) => r.json()),
);
}
Async Queue
class AsyncQueue {
constructor() {
this.queue = [];
this.processing = false;
}
async add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.process();
});
}
async process() {
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
while (this.queue.length > 0) {
const { task, resolve, reject } = this.queue.shift();
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
}
}
this.processing = false;
}
}
// Usage
const queue = new AsyncQueue();
queue.add(() => processItem(1));
queue.add(() => processItem(2));
queue.add(() => processItem(3));
Parallel Limit Map
async function mapWithLimit(items, limit, asyncFn) {
const results = [];
const executing = [];
for (const item of items) {
const promise = asyncFn(item).then((result) => {
executing.splice(executing.indexOf(promise), 1);
return result;
});
results.push(promise);
executing.push(promise);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// Usage
const urls = [...Array(100)].map((_, i) => `/api/item/${i}`);
const results = await mapWithLimit(
urls,
5, // Max 5 concurrent requests
(url) => fetch(url).then((r) => r.json()),
);
Also see
- MDN: async function - Official documentation
- MDN: await operator - Await reference
- JavaScript.info: Async/await - Comprehensive tutorial
- TypeScript: Promises and async/await - TypeScript integration
- Promises cheatsheet - Related cheatsheet
- Fetch API cheatsheet - Using fetch with async/await