Introduction to the Await Keyword
The await keyword is a fundamental part of the async/await syntax in JavaScript. It's what makes asynchronous code read like synchronous code, dramatically improving readability and maintainability.
While we've already been introduced to await in our previous lecture, this session will delve deeper into how it works, its nuanced behavior, and how to effectively handle errors in async functions.
How Await Works
The Await Expression
The await expression causes an async function to pause execution until a Promise is settled (fulfilled or rejected). When the Promise settles, the await expression evaluates to the fulfillment value of the Promise, or throws the rejection reason.
What Can Be Awaited?
The await operator can be used with any "thenable" object – an object with a then() method. This includes native Promises and any Promise-compatible libraries.
// Awaiting a native Promise
const response = await fetch('/api/data');
// Awaiting a Promise created with the constructor
const delay = await new Promise(resolve => setTimeout(resolve, 1000));
// Awaiting a thenable object (not a true Promise, but compatible)
const thenable = {
then(resolve, reject) {
resolve('Result from thenable');
}
};
const result = await thenable; // Works just like a Promise
// Awaiting a non-Promise value (automatically wrapped in a resolved Promise)
const value = await 42; // Equivalent to await Promise.resolve(42)
// Awaiting a function that returns a Promise
async function getData() {
return { id: 1, name: 'Example' };
}
const data = await getData();
When you await a non-Promise value, JavaScript automatically wraps it in a resolved Promise. This makes await very flexible and allows for consistent error handling patterns.
The Execution Flow of Await
To truly master await, it's important to understand exactly how it affects the execution flow of your code.
Step-by-Step Execution
- JavaScript encounters the
awaitexpression in an async function. - If the awaited value is already resolved, execution continues immediately.
- If the awaited value is pending, the function's execution is paused.
- Control is returned to the JavaScript event loop, which can run other code.
- When the awaited Promise settles, the function resumes from where it was paused.
- If the Promise fulfilled, the await expression evaluates to the fulfillment value.
- If the Promise rejected, an exception is thrown at the await line.
// Example to demonstrate execution flow
async function demonstrateAwait() {
console.log('1. Start of function');
console.log('2. Before first await');
const result1 = await Promise.resolve('First result');
console.log('3. After first await:', result1);
console.log('4. Before second await');
try {
const result2 = await Promise.reject('Error occurred');
console.log('This line never executes'); // Skipped due to rejection
} catch (error) {
console.log('5. Caught error:', error);
}
console.log('6. End of function');
return 'Done';
}
console.log('A. Before calling async function');
demonstrateAwait().then(result => {
console.log('C. Async function completed with result:', result);
});
console.log('B. After calling async function');
// Output sequence:
// A. Before calling async function
// 1. Start of function
// 2. Before first await
// B. After calling async function
// 3. After first await: First result
// 4. Before second await
// 5. Caught error: Error occurred
// 6. End of function
// C. Async function completed with result: Done
The Event Loop and Await
Understanding how await interacts with JavaScript's event loop is crucial for mastering asynchronous programming.
// Demonstrating event loop interaction
async function eventLoopDemo() {
console.log('1. Start');
// This timer will be scheduled by the event loop
setTimeout(() => {
console.log('3. Timer callback executed');
}, 0);
// The await causes the function to suspend
// This gives the event loop a chance to process the timer
await Promise.resolve();
console.log('2. After await');
// Another microtask
await Promise.resolve();
console.log('4. After second await');
}
eventLoopDemo();
// Output:
// 1. Start
// 2. After await
// 3. Timer callback executed
// 4. After second await
In the example above, even though the timer is set to 0ms, the timer callback executes after "After await" because:
- The
awaitsuspends the async function and yields to the event loop. - When the Promise resolves, a microtask is queued to resume the function.
- Microtasks have priority over timer callbacks (macrotasks) in the event loop.
- The function resumes and logs "After await" before the timer callback runs.
Error Handling with Await
One of the most significant advantages of async/await is the return to familiar try/catch error handling for asynchronous code.
How Errors Propagate with Await
When a Promise is rejected, the await expression throws an exception that can be caught with a try/catch block.
// Basic error handling with try/catch
async function fetchAndProcessData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return processData(data);
} catch (error) {
console.error('Data processing failed:', error);
// Re-throw specific errors, handle others
if (error.message.includes('HTTP error')) {
throw error; // Re-throw HTTP errors
}
// Return fallback for other errors
return { error: true, message: error.message };
}
}
// Multiple await statements in a single try/catch
async function updateUserProfile(userId, profileData) {
try {
// All these await operations are protected by the same try/catch
const currentUser = await fetchUser(userId);
const validatedData = await validateProfileData(profileData);
const updatedUser = await updateUserInDatabase(userId, validatedData);
await notifyUserOfUpdate(userId);
return updatedUser;
} catch (error) {
// Any error in the chain will be caught here
logError('Profile update failed', error);
throw new Error(`Could not update profile: ${error.message}`);
}
}
Fine-Grained Error Handling
For more precise error handling, you can use nested try/catch blocks to handle errors from specific operations differently.
// Fine-grained error handling with nested try/catch
async function importUserData(userId, dataFile) {
try {
// Fetch the user first
const user = await fetchUser(userId);
try {
// Parse the data file (might fail)
const userData = await parseDataFile(dataFile);
try {
// Import data to database (might fail)
const result = await importToDatabase(user, userData);
return result;
} catch (importError) {
// Only catches errors from importToDatabase
console.error('Database import failed:', importError);
await notifyAdmin('import_failure', { user, error: importError });
throw new Error(`Import failed: ${importError.message}`);
}
} catch (parseError) {
// Only catches errors from parseDataFile
console.error('Data parsing failed:', parseError);
return {
success: false,
phase: 'parsing',
error: parseError.message
};
}
} catch (userError) {
// Only catches errors from fetchUser
console.error('User retrieval failed:', userError);
return {
success: false,
phase: 'user_fetch',
error: userError.message
};
}
}
While nested try/catch blocks provide fine-grained control, they can make code harder to read and maintain. Use them judiciously.
Advanced Error Handling Patterns
The Finally Block
The finally block executes after the try/catch completes, regardless of whether an error was thrown. This is ideal for cleanup operations.
// Using finally for cleanup
async function processFileUpload(file) {
const tempFilePath = await createTempFile();
try {
await saveFileToTemp(file, tempFilePath);
const processedData = await processFile(tempFilePath);
await saveToDatabase(processedData);
return { success: true, data: processedData };
} catch (error) {
console.error('File processing failed:', error);
return { success: false, error: error.message };
} finally {
// This runs regardless of success or failure
try {
await deleteTempFile(tempFilePath);
console.log('Temp file deleted');
} catch (cleanupError) {
// Handle cleanup errors separately to avoid masking the main error
console.warn('Failed to delete temp file:', cleanupError);
}
}
}
Error Types and Custom Errors
Use error types and custom error classes to provide more context and enable selective error handling.
// Custom error classes for better error handling
class APIError extends Error {
constructor(message, statusCode, endpoint) {
super(message);
this.name = 'APIError';
this.statusCode = statusCode;
this.endpoint = endpoint;
this.date = new Date();
}
isClientError() {
return this.statusCode >= 400 && this.statusCode < 500;
}
isServerError() {
return this.statusCode >= 500;
}
}
class ValidationError extends Error {
constructor(message, fieldErrors = {}) {
super(message);
this.name = 'ValidationError';
this.fieldErrors = fieldErrors;
}
}
// Using custom errors in async functions
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new APIError(
`Failed to fetch user ${userId}`,
response.status,
`/api/users/${userId}`
);
}
const userData = await response.json();
// Validate the response
const errors = validateUserData(userData);
if (Object.keys(errors).length > 0) {
throw new ValidationError('Invalid user data', errors);
}
return userData;
} catch (error) {
if (error instanceof APIError) {
if (error.isClientError()) {
// Handle client errors (like 404, 403)
console.error(`API client error: ${error.message}`);
} else if (error.isServerError()) {
// Handle server errors (like 500)
console.error(`API server error: ${error.message}`);
await reportServerError(error);
}
} else if (error instanceof ValidationError) {
// Handle validation errors
console.error('Validation failed:', error.fieldErrors);
} else {
// Handle unexpected errors
console.error('Unexpected error:', error);
}
throw error; // Re-throw for upstream handling
}
}
Retry Pattern with Await
Implement retry logic for operations that might fail temporarily, such as network requests.
// Retry pattern with exponential backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let retries = 0;
while (true) {
try {
const response = await fetch(url, options);
if (!response.ok) {
// For 5xx errors, we'll retry
if (response.status >= 500 && retries < maxRetries) {
throw new Error(`Server error: ${response.status}`);
}
// For other status codes, fail immediately
throw new Error(`Request failed with status: ${response.status}`);
}
return await response.json();
} catch (error) {
retries++;
if (retries > maxRetries) {
console.error(`Failed after ${maxRetries} retries:`, error);
throw error;
}
// Calculate exponential backoff with jitter
const delay = Math.min(1000 * Math.pow(2, retries), 10000)
+ Math.floor(Math.random() * 1000);
console.warn(`Attempt ${retries} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Circuit Breaker Pattern
Implement a circuit breaker to prevent cascading failures when a service is experiencing issues.
// Circuit breaker pattern
class CircuitBreaker {
constructor(action, options = {}) {
this.action = action;
this.failureThreshold = options.failureThreshold || 3;
this.resetTimeout = options.resetTimeout || 30000;
this.failureCount = 0;
this.isOpen = false;
this.lastFailureTime = null;
}
async execute(...args) {
if (this.isOpen) {
// Check if circuit should be reset (half-open)
if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
this.isOpen = false;
} else {
throw new Error('Circuit is open - service unavailable');
}
}
try {
const result = await this.action(...args);
this.failureCount = 0; // Reset on success
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.isOpen = true;
console.error(`Circuit opened after ${this.failureCount} failures`);
}
throw error;
}
}
}
// Using the circuit breaker
const serviceCall = new CircuitBreaker(
async (endpoint) => {
const response = await fetch(`https://api.example.com/${endpoint}`);
if (!response.ok) throw new Error(`Service error: ${response.status}`);
return response.json();
},
{ failureThreshold: 5, resetTimeout: 60000 }
);
async function getUserData(userId) {
try {
return await serviceCall.execute(`users/${userId}`);
} catch (error) {
if (error.message.includes('Circuit is open')) {
return getFallbackUserData(userId);
}
throw error;
}
}
Common Await Pitfalls and Mistakes
Even experienced developers can make mistakes with async/await. Here are some common pitfalls to avoid.
Forgetting to Await
One of the most common mistakes is forgetting to use the await keyword with a Promise.
// INCORRECT: Forgetting to await
async function updateUser(userId, data) {
try {
// No await here! This just returns a Promise without waiting for it
const result = saveToDatabase(userId, data);
// This runs immediately, before the Promise resolves!
console.log('User updated successfully');
return { success: true };
} catch (error) {
// This catch block will never handle saveToDatabase errors
console.error('Failed to update user:', error);
return { success: false, error: error.message };
}
}
// CORRECT: Using await properly
async function updateUser(userId, data) {
try {
// Wait for the Promise to resolve
const result = await saveToDatabase(userId, data);
// This runs after the Promise resolves
console.log('User updated successfully');
return { success: true };
} catch (error) {
console.error('Failed to update user:', error);
return { success: false, error: error.message };
}
}
Sequential vs. Concurrent Await
Using await sequentially when operations could run concurrently can hurt performance.
// INEFFICIENT: Sequential awaits when operations could run concurrently
async function loadUserDashboard(userId) {
// Each operation waits for the previous one to complete
const userData = await fetchUserProfile(userId);
const userPosts = await fetchUserPosts(userId);
const userAnalytics = await fetchUserAnalytics(userId);
return {
user: userData,
posts: userPosts,
analytics: userAnalytics
};
}
// BETTER: Concurrent operations with Promise.all
async function loadUserDashboard(userId) {
// Start all three operations simultaneously
const [userData, userPosts, userAnalytics] = await Promise.all([
fetchUserProfile(userId),
fetchUserPosts(userId),
fetchUserAnalytics(userId)
]);
return {
user: userData,
posts: userPosts,
analytics: userAnalytics
};
}
Swallowing Errors
Catching errors without proper handling or logging can make debugging difficult.
// BAD: Swallowing errors without proper handling
async function processPayment(orderId) {
try {
const result = await chargeCustomer(orderId);
return result;
} catch (error) {
// Error is caught but not logged or handled properly
return { success: false }; // Details of the error are lost!
}
}
// BETTER: Proper error handling
async function processPayment(orderId) {
try {
const result = await chargeCustomer(orderId);
return result;
} catch (error) {
// Log the error with context
console.error(`Payment processing failed for order ${orderId}:`, error);
// Include error details in the response
return {
success: false,
error: error.message,
errorCode: error.code || 'UNKNOWN_ERROR',
orderId: orderId
};
}
}
Uncaught Promise Rejections
Always handle potential rejections from async functions to avoid uncaught exceptions.
// BAD: Not handling potential rejection
const button = document.getElementById('submit-button');
button.addEventListener('click', async function() {
// If this throws, the error won't be caught anywhere!
const result = await submitForm();
showSuccess(result);
});
// BETTER: Always include try/catch in async event handlers
button.addEventListener('click', async function() {
try {
const result = await submitForm();
showSuccess(result);
} catch (error) {
console.error('Form submission failed:', error);
showError(error.message);
}
});
Async Callback Functions
Be careful when using async callbacks with functions that aren't designed for Promises.
// PROBLEMATIC: Array methods don't wait for async callbacks
async function processItems(items) {
// forEach doesn't wait for async callbacks to complete!
items.forEach(async (item) => {
await processItem(item); // These run in parallel, but results are lost
});
console.log('All items processed'); // This runs immediately, not after processing!
}
// BETTER: Use a for...of loop with await
async function processItems(items) {
for (const item of items) {
await processItem(item); // Each iteration waits for processItem to complete
}
console.log('All items processed'); // This runs after all processing is done
}
// ALTERNATIVE: Use Promise.all if parallel processing is desired
async function processItemsConcurrently(items) {
await Promise.all(items.map(item => processItem(item)));
console.log('All items processed'); // This runs after all processing is done
}
Debugging Async/Await Code
Debugging asynchronous code can be challenging, but async/await makes it significantly easier compared to callbacks or raw Promises.
Setting Breakpoints
With async/await, you can set breakpoints at any line, including before and after await expressions.
async function fetchData() {
// Set a breakpoint here to inspect before the fetch
const response = await fetch('/api/data');
// Set a breakpoint here to inspect the response
const data = await response.json();
// Set a breakpoint here to inspect the parsed data
return processData(data);
}
Observing Promise States
Log the state of Promises at different stages to understand asynchronous flow.
async function debugPromises() {
// Create a Promise
const promise = new Promise(resolve => setTimeout(() => resolve('Done'), 1000));
console.log('Initial promise state:', promise);
// Output: Promise {}
// Wait for it to resolve
const result = await promise;
console.log('After await, promise state:', promise);
// Output: Promise {: "Done"}
console.log('Result:', result);
// Output: Done
}
Try/Catch for Debugging
Use try/catch blocks to catch and inspect errors during development.
// Debug helper function
async function debugAwait(promise, label = 'Promise') {
try {
console.log(`${label} - Starting`);
const result = await promise;
console.log(`${label} - Resolved:`, result);
return result;
} catch (error) {
console.error(`${label} - Rejected:`, error);
throw error; // Re-throw to maintain normal flow
}
}
// Using the debug helper
async function fetchUserData(userId) {
try {
const userPromise = fetch(`/api/users/${userId}`);
const response = await debugAwait(userPromise, 'User Fetch');
const dataPromise = response.json();
const userData = await debugAwait(dataPromise, 'JSON Parse');
return userData;
} catch (error) {
console.error('Operation failed:', error);
return null;
}
}
Async Stack Traces
Modern browsers and Node.js provide better stack traces for async functions, showing the async call chain.
// Modern browsers show the complete async stack trace
async function level3() {
throw new Error('Something went wrong');
}
async function level2() {
await level3();
}
async function level1() {
await level2();
}
// This will show a stack trace through the async calls
level1().catch(error => {
console.error('Error with stack trace:', error);
// Error: Something went wrong
// at level3 (script.js:2)
// at async level2 (script.js:6)
// at async level1 (script.js:10)
});
Browser and Environment Support
The await operator is widely supported in modern environments, but considerations for older browsers may be necessary.
Browser Support
- Chrome: Full support since version 55 (December 2016)
- Firefox: Full support since version 52 (March 2017)
- Safari: Full support since version 11 (September 2017)
- Edge: Full support since version 15 (April 2017)
- Internet Explorer: No native support (requires transpilation)
Node.js Support
- Node.js 7.6.0+: Supported without flags
- Node.js 7.0.0 - 7.5.0: Supported with --harmony flag
- Earlier versions: Requires transpilation
Transpilation for Older Environments
For older environments, you can use tools like Babel to transpile async/await code to backwards-compatible alternatives.
// Configure Babel for async/await support
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: {
browsers: [
'last 2 versions',
'not ie <= 11',
'not op_mini all'
],
node: '10'
}
}]
],
plugins: [
'@babel/plugin-transform-runtime'
]
};
Polyfilling Promises
Since async/await relies on Promises, you may need to include a Promise polyfill for very old browsers.
// Using core-js for polyfills
// Installation: npm install core-js regenerator-runtime
// In your entry file:
import 'core-js/stable';
import 'regenerator-runtime/runtime';
Await in Different Contexts
Loops and Await
Different loop types have different behavior with async/await, especially regarding sequential vs. concurrent execution.
(waits for each iteration)"] B --> E["for + await
(waits for each iteration)"] B --> F["while + await
(waits for each iteration)"] C --> G["Promise.all + map
(all run in parallel)"] C --> H["for...of without await
in the loop body
(careful!)"] style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbdefb,stroke:#333,stroke-width:1px style C fill:#c8e6c9,stroke:#333,stroke-width:1px
// Sequential processing with for...of
async function processFilesSequentially(files) {
const results = [];
for (const file of files) {
// Each iteration waits for processing to complete
const result = await processFile(file);
results.push(result);
}
return results;
}
// Concurrent processing with Promise.all
async function processFilesConcurrently(files) {
// Start processing all files at once
const promises = files.map(file => processFile(file));
// Wait for all to complete
const results = await Promise.all(promises);
return results;
}
// Batched processing (controlled concurrency)
async function processFilesInBatches(files, batchSize = 3) {
const results = [];
// Process files in batches
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
// Process current batch concurrently
const batchResults = await Promise.all(batch.map(file => processFile(file)));
results.push(...batchResults);
console.log(`Completed batch ${i / batchSize + 1}`);
}
return results;
}
Conditional Await
Sometimes you need to conditionally await a value based on runtime conditions.
// Conditional await based on value type
async function processValue(value) {
let result;
if (value instanceof Promise) {
// Only await if it's a Promise
result = await value;
} else {
// Use directly if it's already a value
result = value;
}
return result;
}
// Conditional await based on configuration
async function fetchData(endpoint, config) {
// Create base request
let request = fetch(endpoint);
// Conditionally transform the response
if (config.parseJson) {
request = request.then(response => response.json());
} else if (config.parseText) {
request = request.then(response => response.text());
}
// Wait for the final result
return await request;
}
Await in Class Methods
Class methods can be async, enabling clean integration of asynchronous operations in object-oriented code.
// Async methods in classes
class UserService {
constructor(apiClient) {
this.apiClient = apiClient;
this.cache = new Map();
}
async getUser(userId) {
// Check cache first
if (this.cache.has(userId)) {
return this.cache.get(userId);
}
try {
const user = await this.apiClient.fetch(`/users/${userId}`);
this.cache.set(userId, user);
return user;
} catch (error) {
console.error(`Failed to fetch user ${userId}:`, error);
throw new Error(`User retrieval failed: ${error.message}`);
}
}
async updateUser(userId, data) {
try {
const result = await this.apiClient.update(`/users/${userId}`, data);
// Update cache with the new data
if (this.cache.has(userId)) {
const cachedUser = this.cache.get(userId);
this.cache.set(userId, { ...cachedUser, ...data });
}
return result;
} catch (error) {
console.error(`Failed to update user ${userId}:`, error);
throw new Error(`User update failed: ${error.message}`);
}
}
// Getter method can be async too
async get userActivity(userId) {
const user = await this.getUser(userId);
return await this.apiClient.fetch(`/activities?userId=${userId}`);
}
}
Await in Array Methods
Be careful with await in array method callbacks, as they don't automatically wait for asynchronous operations.
// CAUTION: Array methods don't wait for async callbacks!
// This doesn't work as expected - the method returns before async callbacks complete
const processItems = async (items) => {
const results = items.map(async item => {
const processed = await processItem(item);
return processed;
});
// 'results' is an array of Promises, not resolved values!
return results;
};
// Correct way: Wait for all promises with Promise.all
const processItems = async (items) => {
const promises = items.map(async item => {
const processed = await processItem(item);
return processed;
});
// Wait for all promises to resolve
const results = await Promise.all(promises);
return results;
};
// Alternative with filter
const filterExpiredItems = async (items) => {
const checkResults = await Promise.all(
items.map(async item => {
const isExpired = await checkItemExpiration(item);
return { item, isExpired };
})
);
// Filter based on the resolved values
return checkResults
.filter(result => !result.isExpired)
.map(result => result.item);
};
Real-World Examples
Data Fetching in Web Applications
A common use case for await is fetching and processing data from APIs.
// React component with data fetching
async function fetchDashboardData(userId) {
try {
// Fetch user profile
const userResponse = await fetch(`/api/users/${userId}`);
if (!userResponse.ok) {
throw new Error(`Failed to fetch user: ${userResponse.statusText}`);
}
const userData = await userResponse.json();
// Fetch related data concurrently
const [postsData, analyticsData] = await Promise.all([
fetch(`/api/posts?userId=${userId}`)
.then(res => res.ok ? res.json() : []),
fetch(`/api/analytics?userId=${userId}`)
.then(res => res.ok ? res.json() : { views: 0, engagement: 0 })
]);
// Combine all data
return {
user: userData,
posts: postsData,
analytics: analyticsData,
lastUpdated: new Date().toISOString()
};
} catch (error) {
console.error('Dashboard data fetching failed:', error);
// Return fallback data
return {
user: { id: userId, name: 'Unknown User' },
posts: [],
analytics: { views: 0, engagement: 0 },
error: error.message,
lastUpdated: new Date().toISOString()
};
}
}
Transaction Processing
Await is perfect for handling multi-step transactions that need to be processed in order.
// E-commerce order processing
async function processOrder(orderId) {
// Start a database transaction
const transaction = await db.beginTransaction();
try {
// 1. Get the order
const order = await db.orders.findOne(
{ _id: orderId },
{ transaction }
);
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
// 2. Check inventory for all items
for (const item of order.items) {
const product = await db.products.findOne(
{ _id: item.productId },
{ transaction }
);
if (!product || product.stock < item.quantity) {
throw new Error(`Insufficient stock for product ${item.productId}`);
}
// Update inventory
await db.products.update(
{ _id: item.productId },
{ $inc: { stock: -item.quantity } },
{ transaction }
);
}
// 3. Process payment
const paymentResult = await paymentGateway.charge({
amount: order.total,
source: order.paymentMethod,
description: `Order #${order.orderNumber}`
});
if (!paymentResult.success) {
throw new Error(`Payment failed: ${paymentResult.error}`);
}
// 4. Update order status
await db.orders.update(
{ _id: orderId },
{
$set: {
status: 'paid',
paymentId: paymentResult.id,
processedAt: new Date()
}
},
{ transaction }
);
// 5. Commit the transaction
await transaction.commit();
// 6. Send confirmation
await sendOrderConfirmation(order);
return {
success: true,
orderId: orderId,
paymentId: paymentResult.id
};
} catch (error) {
// Roll back the transaction on any error
await transaction.rollback();
console.error('Order processing failed:', error);
// Notify customer service for specific errors
if (error.message.includes('Insufficient stock')) {
await notifyInventoryIssue(order, error);
} else if (error.message.includes('Payment failed')) {
await notifyPaymentIssue(order, error);
}
return {
success: false,
orderId: orderId,
error: error.message
};
}
}
Animation Sequences
Await can simplify complex animation sequences by making timing and sequencing more intuitive.
// Animation sequence with await
async function animateLoginSequence(formElement, successCallback) {
// Helper function for timing
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
try {
// Validate form
const isValid = validateForm(formElement);
if (!isValid) {
throw new Error('Form validation failed');
}
// 1. Disable the form and show loading
formElement.classList.add('form-disabled');
const loadingSpinner = document.getElementById('loading-spinner');
loadingSpinner.style.display = 'block';
// 2. Submit the form
const formData = new FormData(formElement);
const response = await fetch('/api/login', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Login failed: ${response.statusText}`);
}
const userData = await response.json();
// 3. Hide loading, show success checkmark
loadingSpinner.style.display = 'none';
const successMark = document.getElementById('success-mark');
successMark.style.display = 'block';
successMark.classList.add('animate-in');
// 4. Wait for animation to complete
await wait(1000);
// 5. Fade out the form
formElement.style.transition = 'opacity 0.5s ease-out';
formElement.style.opacity = '0';
await wait(500);
// 6. Replace with welcome message
const welcomeMessage = document.getElementById('welcome-message');
welcomeMessage.textContent = `Welcome, ${userData.name}!`;
welcomeMessage.style.opacity = '0';
welcomeMessage.style.display = 'block';
// 7. Fade in welcome message
welcomeMessage.style.transition = 'opacity 0.5s ease-in';
welcomeMessage.style.opacity = '1';
await wait(500);
// 8. Complete the sequence
successCallback(userData);
} catch (error) {
// Handle animation errors
const errorMessage = document.getElementById('error-message');
errorMessage.textContent = error.message;
errorMessage.style.display = 'block';
// Reset form
formElement.classList.remove('form-disabled');
document.getElementById('loading-spinner').style.display = 'none';
// Shake the form to indicate error
formElement.classList.add('shake-animation');
await wait(500);
formElement.classList.remove('shake-animation');
}
}
Practice Exercises
Exercise 1: Error Handling with Await
Write an async function that attempts to fetch data from multiple fallback sources, handling errors appropriately.
Exercise 2: Concurrent vs. Sequential Processing
Implement two versions of a function that processes an array of files: one that processes them sequentially and another that processes them concurrently. Compare the performance.
Exercise 3: Custom Error Handling
Create a custom error class for API errors and implement an async function that uses it to provide detailed error information.
Exercise 4: Retry Pattern
Implement a retry mechanism for an async function that might fail temporarily. Include exponential backoff and a maximum retry count.
Summary
The await keyword is a powerful tool for managing asynchronous operations in JavaScript:
- Functionality: Pauses execution until a Promise settles, then returns the fulfillment value or throws the rejection reason.
- Error Handling: Enables the use of traditional try/catch blocks for asynchronous error handling.
- Execution Flow: Affects the event loop by yielding control during the await operation, allowing other code to execute.
- Flexibility: Can await any "thenable" object, including native Promises and Promise-like objects.
- Patterns: Common patterns include fine-grained error handling, retry mechanisms, fallbacks, and circuit breakers.
- Pitfalls: Avoid common mistakes like forgetting to await, swallowing errors, and inadvertently sequential execution.
By mastering the await keyword and its error handling capabilities, you can write asynchronous code that is cleaner, more maintainable, and more robust, handling even complex error scenarios with grace and precision.