JavaScript Promises Tutorial - Section 4: Async/Await
Async/await allows you to use traditional try/catch blocks for error handling, making async code look and behave like synchronous code.
With Promises:
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => console.log(posts))
.catch(error => console.error(error));
With Async/Await:
async function getUserPosts() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
console.log(posts);
} catch (error) {
console.error(error);
}
}
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
}
async function processOrder(orderId) {
try {
const order = await fetchOrder(orderId);
const payment = await processPayment(order);
const confirmation = await sendConfirmation(payment);
return confirmation;
} catch (error) {
console.error('Order processing failed:', error);
// Rollback logic here
throw error;
}
}
async function loadDashboard(userId) {
try {
const [user, posts, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId)
]);
return { user, posts, notifications };
} catch (error) {
console.error('Dashboard load failed:', error);
throw error;
}
}
Handle different error types with multiple try/catch blocks:
async function complexOperation() {
let user;
// Try to get user
try {
user = await fetchUser(1);
} catch (error) {
console.error('User fetch failed:', error);
user = getDefaultUser();
}
// Try to get posts
try {
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (error) {
console.error('Posts fetch failed:', error);
return { user, posts: [] };
}
}
async function getUserData(userId) {
try {
return await fetchUser(userId);
} catch (error) {
console.warn('Using cached user data');
return getCachedUser(userId);
}
}
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
console.log(`Retry ${i + 1}/${retries}`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
async function loadPageData() {
const data = {
critical: null,
optional: null
};
try {
data.critical = await fetchCriticalData();
} catch (error) {
console.error('Critical data failed:', error);
throw error; // Can't continue without critical data
}
try {
data.optional = await fetchOptionalData();
} catch (error) {
console.warn('Optional data failed, continuing anyway');
data.optional = null;
}
return data;
}
Use finally for cleanup operations that should run regardless of success or failure:
async function processFile(filename) {
let file;
try {
file = await openFile(filename);
const data = await readFile(file);
await processData(data);
} catch (error) {
console.error('File processing failed:', error);
throw error;
} finally {
if (file) {
await closeFile(file);
console.log('File closed');
}
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new NetworkError(
'Request failed',
response.status
);
}
return await response.json();
} catch (error) {
if (error instanceof NetworkError) {
if (error.statusCode === 404) {
console.error('Resource not found');
} else if (error.statusCode === 500) {
console.error('Server error');
}
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
async function submitForm(formData) {
const submitButton = document.querySelector('#submit');
const errorDiv = document.querySelector('#error');
const successDiv = document.querySelector('#success');
try {
submitButton.disabled = true;
errorDiv.textContent = '';
// Validate
await validateFormData(formData);
// Submit
const response = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
successDiv.textContent = 'Form submitted successfully!';
return result;
} catch (error) {
errorDiv.textContent = `Error: ${error.message}`;
console.error('Form submission failed:', error);
throw error;
} finally {
submitButton.disabled = false;
}
}
// Bad: Unhandled rejection
async function bad() {
const data = await fetchData(); // Can throw!
return data;
}
// Good: Proper error handling
async function good() {
try {
const data = await fetchData();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// Bad: Silently swallows errors
async function bad() {
try {
return await fetchData();
} catch (error) {
console.error(error);
// Error is lost!
}
}
// Good: Re-throw or return fallback
async function good() {
try {
return await fetchData();
} catch (error) {
console.error(error);
throw error; // Or return fallback
}
}
// Bad: Cleanup might not run
async function bad() {
showSpinner();
try {
await fetchData();
hideSpinner(); // Won't run if error!
} catch (error) {
hideSpinner(); // Duplicate code
throw error;
}
}
// Good: Finally ensures cleanup
async function good() {
showSpinner();
try {
await fetchData();
} catch (error) {
console.error(error);
throw error;
} finally {
hideSpinner(); // Always runs
}
}
try/catch for error handling with async/awaitfinally block always runs for cleanupNext, we'll dive deeper into async functions and their advanced features!