Async/await is syntactic sugar built on top of Promises, introduced in ES2017 (ES8). It allows you to write asynchronous code that looks and behaves like synchronous code, making it incredibly readable and maintainable. With async/await, you can avoid .then() chains and write async operations as if they were blocking, while still being non-blocking under the hood. It's the most elegant way to handle async operations in modern JavaScript. Let's master async/await!
The async Keyword
The async keyword before a function makes it return a Promise automatically.
// Regular function
function regularFunction() {
return 'Hello';
}
console.log(regularFunction()); // "Hello"
// Async function
async function asyncFunction() {
return 'Hello';
}
console.log(asyncFunction()); // Promise { 'Hello' }
// The return value is automatically wrapped in a Promise!
// Consuming async function
asyncFunction().then(result => {
console.log(result); // "Hello"
});
// Even if you explicitly return a Promise
async function explicitPromise() {
return Promise.resolve('Hi');
}
explicitPromise().then(result => {
console.log(result); // "Hi" (not nested Promise)
});Async Functions Always Return Promises
// Different ways to declare async functions
// Function declaration
async function fetchData() {
return 'data';
}
// Function expression
const fetchData2 = async function() {
return 'data';
};
// Arrow function
const fetchData3 = async () => {
return 'data';
};
// Async method in object
const obj = {
async getData() {
return 'data';
}
};
// Async method in class
class DataFetcher {
async fetchData() {
return 'data';
}
}
// All of these return Promises!
fetchData().then(d => console.log(d));
fetchData2().then(d => console.log(d));
fetchData3().then(d => console.log(d));
obj.getData().then(d => console.log(d));
new DataFetcher().fetchData().then(d => console.log(d));The await Keyword
The await keyword pauses async function execution until a Promise resolves, then returns the resolved value.
// Without await (Promise-based)
function getData() {
return fetchData().then(data => {
return processData(data);
}).then(result => {
return saveData(result);
});
}
// With await (cleaner!)
async function getData() {
let data = await fetchData(); // Wait for promise
let result = await processData(data); // Wait for promise
await saveData(result); // Wait for promise
return 'Done';
}
// await makes async code look synchronous!
// Example with real delay
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function demo() {
console.log('Start');
await delay(2000); // Wait 2 seconds
console.log('After 2 seconds');
await delay(1000); // Wait 1 second
console.log('After 3 seconds total');
}
demo();
// Output:
// Start
// (2 seconds pause...)
// After 2 seconds
// (1 second pause...)
// After 3 seconds totalawait Unwraps Promises
async function example() {
// Promise.resolve returns a Promise
let promise = Promise.resolve('Hello');
// await unwraps the Promise, giving you the value
let value = await promise;
console.log(value); // "Hello" (not a Promise!)
// You can use the value directly
console.log(value.toUpperCase()); // "HELLO"
}
// Without await, you'd need .then()
function exampleWithoutAwait() {
let promise = Promise.resolve('Hello');
promise.then(value => {
console.log(value);
console.log(value.toUpperCase());
});
}
// await is much cleaner!Important: await can only be used inside async functions (or at the top level of ES modules). Using it elsewhere causes a syntax error.
Promise Chains vs Async/Await
Async/await is more readable
Promise Chains
// Promise chains (.then)
function getUserData(userId) {
return getUser(userId)
.then(user => {
console.log('User:', user.name);
return getPosts(user.id);
})
.then(posts => {
console.log('Posts:', posts.length);
return getComments(posts[0].id);
})
.then(comments => {
console.log('Comments:', comments.length);
return comments;
})
.catch(error => {
console.log('Error:', error.message);
throw error;
});
}
// Still readable, but...
// Multiple .then() calls
// Need to return promises
// Nesting for multiple operationsAsync/Await
// Async/await (cleaner!)
async function getUserData(userId) {
try {
let user = await getUser(userId);
console.log('User:', user.name);
let posts = await getPosts(user.id);
console.log('Posts:', posts.length);
let comments = await getComments(posts[0].id);
console.log('Comments:', comments.length);
return comments;
} catch (error) {
console.log('Error:', error.message);
throw error;
}
}
// Looks synchronous!
// Easy to read top-to-bottom
// Natural error handlingError Handling with Try-Catch
Basic Try-Catch
async function fetchUserData(userId) {
try {
let user = await getUser(userId);
console.log('User:', user.name);
return user;
} catch (error) {
console.log('Error fetching user:', error.message);
// Handle error, return default, or re-throw
return null;
}
}
// Multiple operations
async function processData() {
try {
let data = await fetchData();
let processed = await processStep1(data);
let final = await processStep2(processed);
return final;
} catch (error) {
console.log('Processing failed:', error.message);
throw error; // Re-throw for caller to handle
}
}
// Usage
processData()
.then(result => console.log('Success:', result))
.catch(error => console.log('Failed:', error));Specific Error Handling
async function loadUserProfile(userId) {
try {
let user = await getUser(userId);
try {
let avatar = await loadAvatar(user.avatarUrl);
return { user, avatar };
} catch (avatarError) {
// Avatar failed, but continue with default
console.log('Avatar failed, using default');
return { user, avatar: null };
}
} catch (userError) {
// User fetch failed - critical error
console.log('Failed to load user:', userError.message);
throw userError;
}
}
// Handling specific error types
async function fetchData(url) {
try {
let response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
let data = await response.json();
return data;
} catch (error) {
if (error.name === 'TypeError') {
console.log('Network error:', error.message);
} else if (error.message.includes('HTTP')) {
console.log('Server error:', error.message);
} else {
console.log('Unknown error:', error.message);
}
throw error;
}
}Finally Block
async function loadData() {
showLoadingSpinner();
try {
let data = await fetchData();
let processed = await processData(data);
displayData(processed);
} catch (error) {
showError(error.message);
} finally {
// Always runs, regardless of success or failure
hideLoadingSpinner();
console.log('Loading complete');
}
}
// Finally is perfect for cleanup
async function updateDatabase() {
let connection = await connectToDatabase();
try {
await connection.beginTransaction();
await connection.query('UPDATE ...');
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
// Always close connection
await connection.close();
}
}Parallel Execution
Sequential vs Parallel
// Sequential (one after another)
async function sequential() {
console.time('Sequential');
let user = await getUser(1); // Wait 1 second
let posts = await getPosts(1); // Wait 1 second
let comments = await getComments(1); // Wait 1 second
console.timeEnd('Sequential'); // ~3 seconds
return { user, posts, comments };
}
// Parallel (all at once!)
async function parallel() {
console.time('Parallel');
// Start all promises simultaneously
let [user, posts, comments] = await Promise.all([
getUser(1),
getPosts(1),
getComments(1)
]);
console.timeEnd('Parallel'); // ~1 second (fastest)
return { user, posts, comments };
}
// When operations don't depend on each other, run them in parallel!Starting Promises Before Awaiting
// Inefficient: Sequential
async function inefficient() {
let data1 = await fetchData1(); // Wait
let data2 = await fetchData2(); // Wait
let data3 = await fetchData3(); // Wait
return [data1, data2, data3];
}
// Efficient: Start all, then await all
async function efficient() {
// Start all promises (don't await yet)
let promise1 = fetchData1();
let promise2 = fetchData2();
let promise3 = fetchData3();
// Now await all (they're already running!)
let data1 = await promise1;
let data2 = await promise2;
let data3 = await promise3;
return [data1, data2, data3];
}
// Or use Promise.all
async function withPromiseAll() {
let results = await Promise.all([
fetchData1(),
fetchData2(),
fetchData3()
]);
return results;
}Common Async/Await Patterns
Pattern 1: Conditional Async Operations
async function loadUserData(userId, options = {}) {
let user = await getUser(userId);
let data = { user };
// Conditionally load additional data
if (options.includePosts) {
data.posts = await getPosts(userId);
}
if (options.includeComments) {
data.comments = await getComments(userId);
}
if (options.includeFriends) {
data.friends = await getFriends(userId);
}
return data;
}
// Usage
let basicData = await loadUserData(1);
let fullData = await loadUserData(1, {
includePosts: true,
includeComments: true,
includeFriends: true
});Pattern 2: Retry Logic
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
let response = await fetch(url);
return await response.json();
} catch (error) {
console.log(`Attempt ${i + 1} failed`);
if (i === maxRetries - 1) {
throw error; // Last attempt failed
}
// Wait before retrying (exponential backoff)
await delay(1000 * Math.pow(2, i));
}
}
}
// Usage
try {
let data = await fetchWithRetry('/api/data');
console.log('Data loaded:', data);
} catch (error) {
console.log('All retries failed:', error);
}Pattern 3: Sequential Processing with Accumulation
async function processUsers(userIds) {
let results = [];
for (let userId of userIds) {
try {
let user = await getUser(userId);
let processed = await processUser(user);
results.push(processed);
} catch (error) {
console.log(`Failed to process user ${userId}`);
results.push(null);
}
}
return results;
}
// Process in batches
async function processBatch(items, batchSize = 5) {
let results = [];
for (let i = 0; i < items.length; i += batchSize) {
let batch = items.slice(i, i + batchSize);
// Process batch in parallel
let batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
}
return results;
}Pattern 4: Race with Timeout
async function fetchWithTimeout(url, timeoutMs = 5000) {
let timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Request timeout'));
}, timeoutMs);
});
try {
let response = await Promise.race([
fetch(url),
timeoutPromise
]);
return await response.json();
} catch (error) {
if (error.message === 'Request timeout') {
console.log('Request took too long');
}
throw error;
}
}
// Usage
try {
let data = await fetchWithTimeout('/api/data', 3000);
console.log('Data:', data);
} catch (error) {
console.log('Failed:', error.message);
}Practical Examples
Example 1: User Dashboard
async function loadUserDashboard(userId) {
try {
// Show loading state
showLoading();
// Load user data first (needed for other requests)
let user = await getUser(userId);
displayUserHeader(user);
// Load other data in parallel (doesn't depend on each other)
let [posts, stats, friends, notifications] = await Promise.all([
getUserPosts(userId),
getUserStats(userId),
getUserFriends(userId),
getUserNotifications(userId)
]);
// Display everything
displayPosts(posts);
displayStats(stats);
displayFriends(friends);
displayNotifications(notifications);
console.log('Dashboard loaded successfully');
} catch (error) {
console.error('Failed to load dashboard:', error);
showError('Could not load dashboard. Please try again.');
} finally {
hideLoading();
}
}
// Usage
loadUserDashboard(currentUserId);Example 2: Form Submission with Validation
async function submitRegistrationForm(formData) {
try {
// Client-side validation
validateFormData(formData);
// Check if email exists (async validation)
let emailExists = await checkEmailExists(formData.email);
if (emailExists) {
throw new Error('Email already registered');
}
// Check if username is available
let usernameAvailable = await checkUsernameAvailable(formData.username);
if (!usernameAvailable) {
throw new Error('Username already taken');
}
// Submit the form
let response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error('Registration failed');
}
let result = await response.json();
// Success
showSuccess('Registration successful!');
redirectToLogin();
return result;
} catch (error) {
showError(error.message);
throw error;
}
}
// Usage
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
let formData = {
username: e.target.username.value,
email: e.target.email.value,
password: e.target.password.value
};
try {
await submitRegistrationForm(formData);
} catch (error) {
console.log('Registration failed');
}
});Async/Await Mastery
Modern asynchronous JavaScript
console.log() to see your output in the console above.Key Takeaways
asynckeyword makes function return a Promiseawaitpauses execution until Promise resolves- await can only be used inside async functions
- Async/await makes async code look synchronous
- Use try-catch for error handling (natural and readable)
- Use
finallyfor cleanup operations - Sequential: await one at a time (slower)
- Parallel: use Promise.all for independent operations (faster)
- Async functions are built on Promises (syntactic sugar)
- Modern standard for async JavaScript—use it!
What's Next?
Congratulations! You've completed the Asynchronous JavaScript section and now understand the full evolution of async patterns—from callbacks to Promises to async/await. You've learned the most elegant way to write async code in modern JavaScript!
You've now completed 47 out of 62 JavaScript tutorial pages (76% complete)! You've mastered 10 major sections including async programming. You're well on your way to JavaScript mastery!
💪 Practice Challenge:
Before moving on, try creating:
- Convert Promise-based code to async/await
- Create an async function with proper try-catch error handling
- Load multiple resources in parallel using Promise.all with await
- Implement retry logic with async/await
- Build a user dashboard that loads data with async/await