JavaScript Promises and Async/Await
JavaScript is single-threaded, but many operations like API calls or file reading happen asynchronously. Promises and async/await make handling these asynchronous operations much easier. This tutorial covers how to work with asynchronous JavaScript, from basic promises to modern async/await syntax.
Why Asynchronous JavaScript?
JavaScript runs in a single thread, so blocking operations would freeze the entire application. Asynchronous programming allows the code to continue executing while waiting for operations like network requests, file I/O, or timers to complete.
Understanding Callbacks
Before promises, callbacks were the primary way to handle asynchronous operations.
// Traditional callback approach
function fetchUser(id, callback) {
setTimeout(() => {
const user = { id, name: 'John Doe' };
callback(null, user);
}, 1000);
}
fetchUser(1, (error, user) => {
if (error) {
console.error('Error:', error);
} else {
console.log('User:', user);
}
});Callbacks can lead to “callback hell” when chaining multiple async operations.
What are Promises?
A Promise represents the eventual completion (or failure) of an asynchronous operation. It can be in one of three states:
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation completed!');
} else {
reject('Operation failed!');
}
}, 1000);
});
// Using the Promise
myPromise
.then(result => console.log(result))
.catch(error => console.error(error));Promise Methods
.then() and .catch()
Chain .then() for success and .catch() for errors.
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));.finally()
Runs regardless of success or failure.
fetch('/api/data')
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error))
.finally(() => console.log('Request completed'));Promise.all()
Wait for multiple promises to complete.
const promise1 = fetch('/api/users');
const promise2 = fetch('/api/posts');
const promise3 = fetch('/api/comments');
Promise.all([promise1, promise2, promise3])
.then(responses => {
// All promises fulfilled
return Promise.all(responses.map(r => r.json()));
})
.then(data => console.log('All data:', data))
.catch(error => console.error('One promise failed:', error));Promise.race()
Returns the first promise that settles (either fulfills or rejects).
const fastAPI = fetch('/api/fast');
const slowAPI = fetch('/api/slow');
Promise.race([fastAPI, slowAPI])
.then(response => response.json())
.then(data => console.log('Fastest response:', data));Promise.allSettled()
Waits for all promises to settle, regardless of success or failure.
Promise.allSettled([promise1, promise2, promise3])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Fulfilled:', result.value);
} else {
console.log('Rejected:', result.reason);
}
});
});Async/Await Syntax
Async/await is syntactic sugar built on top of promises, making asynchronous code look synchronous.
Basic async/await
async function getUser() {
try {
const response = await fetch('/api/user');
const user = await response.json();
console.log('User:', user);
} catch (error) {
console.error('Error:', error);
}
}
getUser();Async functions always return promises
async function sayHello() {
return 'Hello!';
}
sayHello().then(message => console.log(message)); // "Hello!"
Error handling with try/catch
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
throw error; // Re-throw to let caller handle
}
}Parallel operations with async/await
async function getAllData() {
try {
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())
]);
console.log('Users:', users);
console.log('Posts:', posts);
console.log('Comments:', comments);
} catch (error) {
console.error('Error fetching data:', error);
}
}Converting Callbacks to Promises
Many older APIs use callbacks. You can wrap them in promises.
// Callback-based function
function oldAPI(callback) {
setTimeout(() => {
callback(null, 'result');
}, 1000);
}
// Convert to Promise
function promisifiedAPI() {
return new Promise((resolve, reject) => {
oldAPI((error, result) => {
if (error) reject(error);
else resolve(result);
});
});
}
// Use with async/await
async function useOldAPI() {
try {
const result = await promisifiedAPI();
console.log(result);
} catch (error) {
console.error(error);
}
}Real-world Examples
API Calls
async function loginUser(credentials) {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error('Login failed');
}
const user = await response.json();
return user;
} catch (error) {
console.error('Login error:', error);
throw error;
}
}
// Usage
loginUser({ email: '[email protected]', password: 'secret' })
.then(user => console.log('Logged in:', user))
.catch(error => console.error('Login failed:', error));File Operations (Node.js)
const fs = require('fs').promises;
async function readAndProcessFile(filename) {
try {
const content = await fs.readFile(filename, 'utf8');
const processed = content.toUpperCase();
await fs.writeFile(filename + '.processed', processed);
console.log('File processed successfully');
} catch (error) {
console.error('Error processing file:', error);
}
}Sequential vs Parallel Execution
// Sequential (one after another)
async function sequentialTasks() {
const task1 = await fetch('/api/task1');
const task2 = await fetch('/api/task2');
const task3 = await fetch('/api/task3');
console.log('All tasks completed sequentially');
}
// Parallel (all at once)
async function parallelTasks() {
const [result1, result2, result3] = await Promise.all([
fetch('/api/task1'),
fetch('/api/task2'),
fetch('/api/task3')
]);
console.log('All tasks completed in parallel');
}Best Practices
- Always use try/catch with async/await: Handle errors properly
- Avoid mixing callbacks and promises: Choose one approach consistently
- Use Promise.all for parallel operations: When operations don’t depend on each other
- Return promises from async functions: Don’t await unnecessarily in return statements
- Handle all promise rejections: Use .catch() or try/catch
Common Mistakes
// ❌ Don't do this - swallowing errors
async function badExample() {
const result = await someAsyncOperation(); // No try/catch
}
// ❌ Don't forget await
async function anotherBadExample() {
fetch('/api/data'); // Forgot await - promise is ignored
}
// ✅ Do this instead
async function goodExample() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}Summary
Promises and async/await have revolutionized asynchronous JavaScript:
| Feature | Promises | Async/Await |
|---|---|---|
| Readability | Good | Excellent |
| Error Handling | .catch() | try/catch |
| Chaining | .then() chains | Sequential code |
| Parallel | Promise.all() | Promise.all() |
| Browser Support | Wide | Modern browsers |
Both approaches are valid, but async/await is generally preferred for new code due to its cleaner syntax.
External Resources:
Related Tutorials: