Async/Await Cheat Sheet
Asynchronous operations are fundamental to modern web development, allowing non-blocking execution crucial for responsive UIs and efficient server-side…
Asynchronous operations are fundamental to modern web development, allowing non-blocking execution crucial for responsive UIs and efficient server-side processing. JavaScript's async/await syntax, built atop Promises, provides a synchronous-looking approach to managing asynchronous code, significantly improving readability and maintainability compared to traditional callback patterns or raw Promise chains. This guide covers the core concepts, practical usage patterns, and error handling strategies for async/await in JavaScript and TypeScript.
Introduced in ECMAScript 2017 (ES8), async/await is a syntactic sugar over Promises. An async function always returns a Promise. The await keyword can only be used inside an async function and pauses the execution of that function until the awaited Promise settles (either fulfills or rejects).
Basic Syntax and Execution Flow
An async function is declared with the async keyword before the function keyword (or arrow function parameters). Inside an async function, you can use await to wait for a Promise to resolve.
// Async function declaration
async function fetchData(url: string): Promise<any> {
console.log(`Fetching data from ${url}...`);
const response = await fetch(url); // Pauses here until fetch() Promise resolves
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json(); // Pauses here until json() Promise resolves
console.log(`Data from ${url} received.`);
return data;
}
// Invoking an async function
async function main() {
try {
const userData = await fetchData('https://api.example.com/users/1');
console.log('User data:', userData);
} catch (error) {
console.error('Failed to fetch user data:', error);
}
}
main();
console.log("Main function invoked (this runs immediately, not waiting for fetchData to complete)");
When fetchData is called, it immediately returns a Promise. The main function then awaits this Promise. Inside fetchData, the first await fetch(url) pauses the fetchData function's execution until the network request completes. Once it resolves, the next line executes. This continues until the function returns a value or throws an error, at which point the Promise returned by fetchData is settled.
Error Handling with try...catch
async/await integrates seamlessly with standard try...catch blocks, providing a familiar mechanism for handling errors from asynchronous operations. If any awaited Promise rejects, the error is thrown and can be caught by the nearest catch block.
async function loadUser(id: string): Promise<any> {
try {
console.log(`Attempting to load user ${id}...`);
const res = await fetch(`https://api.example.com/users/${id}`);
// Check for non-2xx HTTP status codes
if (!res.ok) {
console.error(`HTTP error for user ${id}: ${res.status} ${res.statusText}`);
// Throwing an error here ensures it's caught by the outer try/catch
throw new Error(`HTTP ${res.status} for user ${id}`);
}
const userData = await res.json();
console.log(`User ${id} loaded successfully.`);
return userData;
} catch (err: unknown) { // Use 'unknown' for safer type checking in TypeScript
if (err instanceof Error) {
console.error(`loadUser failed for ID ${id}: ${err.message}`);
} else {
console.error(`loadUser failed for ID ${id}: An unknown error occurred.`);
}
// Re-throw the error if callers need to handle it further
throw err;
}
}
async function processUsers() {
console.log("Starting user processing...");
try {
const user1 = await loadUser("123"); // Assuming this succeeds
console.log("Processed user 123:", user1);
const userFail = await loadUser("nonexistent"); // Assuming this fails (e.g., 404)
console.log("Processed user nonexistent:", userFail); // This line won't be reached
} catch (outerError) {
console.error("An error occurred during user processing:", outerError);
}
console.log("User processing finished.");
}
processUsers();
In this example, if fetch fails (e.g., network error) or returns a non-ok status, the catch block within loadUser will execute. If loadUser re-throws the error, the catch block in processUsers will then handle it.
Parallel Execution with Promise.all and Promise.allSettled
Using await sequentially for independent operations will block execution unnecessarily. For concurrent operations, Promise.all and Promise.allSettled are essential.
Promise.all(iterable)
Waits for all Promises in the iterable to fulfill. If any Promise rejects, Promise.all immediately rejects with the reason of the first Promise that rejected. The result is an array of fulfilled values, in the same order as the input Promises.
async function getMultipleUsers(ids: string[]): Promise<any[]> {
try {
console.log(`Loading users in parallel: ${ids.join(', ')}`);
const userPromises = ids.map(id => loadUser(id));
const users = await Promise.all(userPromises); // Waits for all to succeed
console.log('All users loaded successfully.');
return users;
} catch (error) {
console.error('One or more users failed to load:', error);
throw error; // Re-throw to indicate overall failure
}
}
// Example usage
// Assuming loadUser("456") might fail (e.g., 404)
getMultipleUsers(["123", "456", "789"])
.then(users => console.log("Successfully retrieved all users:", users))
.catch(err => console.error("Failed to retrieve some users, but got:", err));
Common use case for Promise.all:
async function initializeApplication() {
try {
const [config, userProfile, permissions] = await Promise.all([
fetch('/api/config').then(res => res.json()),
fetch('/api/profile').then(res => res.json()),
fetch('/api/permissions').then(res => res.json())
]);
console.log('Application initialized with:', { config, userProfile, permissions });
// Proceed with app setup
} catch (error) {
console.error('Failed to initialize application components:', error);
// Render an error page or show a toast notification
}
}
Promise.allSettled(iterable)
Waits for all Promises in the iterable to settle (either fulfill or reject). It never rejects. The result is an array of objects, each describing the outcome of a Promise ({ status: 'fulfilled', value: ... } or { status: 'rejected', reason: ... }), maintaining the input order.
Useful when you want to proceed even if some operations fail, and you need to know the outcome of each individual operation.
async function getPartialUsers(ids: string[]): Promise<PromiseSettledResult<any>[]> {
console.log(`Attempting to load partial users: ${ids.join(', ')}`);
const userPromises = ids.map(id => loadUser(id).catch(err => ({ status: 'rejected', reason: err }))); // loadUser already handles its own error, but for Promise.allSettled it's often cleaner to let it handle raw promises
// A more direct approach with Promise.allSettled:
const results = await Promise.allSettled(ids.map(id => loadUser(id)));
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`User ${ids[index]} loaded:`, result.value);
} else {
console.warn(`User ${ids[index]} failed:`, result.reason);
}
});
return results;
}
getPartialUsers(["userA", "userB", "userC"]) // Assuming userB fails
.then(results => console.log("All partial user load attempts completed:", results))
.catch(err => console.error("Should not happen with Promise.allSettled, but caught:", err));
TypeScript Considerations
TypeScript enhances async/await with strong typing for function return values and error handling.
async functions always returnPromise<T>, whereTis the type of the value the function would synchronously return.- When catching errors, the type of the caught variable in a
catchblock isunknownby default in TypeScript 4.0+. This is a safer default as anything can be thrown. You should narrow the type (e.g., usinginstanceof Error) or assert it.
async function typedFetchData(url: string): Promise<{ id: number; name: string }> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
const data: { id: number; name: string } = await response.json();
return data;
}
async function useTypedData() {
try {
const user = await typedFetchData('https://api.example.com/users/profile');
// 'user' is correctly typed as { id: number; name: string }
console.log(`User ID: ${user.id}, Name: ${user.name}`);
return user;
} catch (error: unknown) {
if (error instanceof Error) {
console.error('Error fetching typed data:', error.message);
} else {
console.error('An unexpected error occurred:', error);
}
throw error;
}
}
Promise.race and Promise.any
Promise.race(iterable)
Returns a Promise that settles as soon as one of the Promises in the iterable settles. Its resolution or rejection matches the first Promise that settles. Useful for timeouts or fetching data from multiple sources, taking the fastest one.
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<any> {
const fetchPromise = fetch(url).then(res => {
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
return res.json();
});
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${timeoutMs} ms for ${url}`)), timeoutMs)
);
return Promise.race([fetchPromise, timeoutPromise]);
}
async function getDataOrTimeout() {
try {
const data = await fetchWithTimeout('https://api.example.com/slow-service', 1000); // 1-second timeout
console.log('Received data within timeout:', data);
} catch (error) {
console.error('Failed to get data or timed out:', error);
}
}
getDataOrTimeout();
Promise.any(iterable) (ES2021+)
Returns a Promise that fulfills as soon as one of the Promises in the iterable fulfills. If all Promises reject, it rejects with an AggregateError, containing all rejection reasons. Useful when you need at least one successful result from multiple independent operations.
async function getFastestSuccessfulData(urls: string[]): Promise<any> {
const fetchPromises = urls.map(url =>
fetch(url).then(res => {
if (!res.ok) throw new Error(`HTTP Error from ${url}: ${res.status}`);
return res.json();
})
);
try {
const data = await Promise.any(fetchPromises);
console.log('Got data from the fastest successful source:', data);
return data;
} catch (error: any) { // Type 'any' for AggregateError compatibility in older TS versions
if (error instanceof AggregateError) {
console.error('All data sources failed:', error.errors.map((e: Error) => e.message));
} else {
console.error('An unexpected error occurred with Promise.any:', error);
}
throw error;
}
}
// Example: Try fetching from primary, then secondary, then tertiary API endpoints
getFastestSuccessfulData([
'https://api.primary.example.com/data',
'https://api.secondary.example.com/data',
'https://api.tertiary.example.com/data'
]);
Common Pitfalls
- Forgetting
await: If you omitawait, the code will continue executing immediately, and you'll be working with a Promise object, not its resolved value. This often leads toTypeError: Cannot read property 'xyz' of undefinedor similar issues when trying to access properties of the unresolved Promise. - Unnecessary sequential execution: Using
awaitin a loop for independent operations makes them run one after another, wasting time. UsePromise.allfor concurrent execution. - Unhandled Promise rejections: If an
asyncfunction rejects and its returned Promise isn't awaited or doesn't have a.catch()handler, it can lead to unhandled promise rejection errors, especially in Node.js environments where this might crash the process by default. - Top-level
await(Node.js >= 14.8, modern browsers): While now supported in module contexts, you generally can't useawaitdirectly at the top level of a script without wrapping it in anasyncfunction, especially in older environments or non-module scripts. - Swallowing errors: Overuse of
try...catchwithout re-throwing or logging can mask failures, making debugging difficult. Ensure errors are either handled meaningfully or propagated.