Async/Await
Callbacks work, but they can get messy. Promises and async/await give you cleaner asynchronous code.
The Callback Problem
// Callback hell (pyramid of doom)
fs.readFile('file1.txt', 'utf8', (err, data1) => {
if (err) return console.error(err);
fs.readFile('file2.txt', 'utf8', (err, data2) => {
if (err) return console.error(err);
fs.readFile('file3.txt', 'utf8', (err, data3) => {
if (err) return console.error(err);
console.log(data1, data2, data3);
});
});
});
Hard to read, hard to maintain. Let's fix this.
Promises Basics
A Promise represents a value that will be available later:
// A Promise has three states:
// - Pending: still working
// - Fulfilled: completed successfully
// - Rejected: failed with an error
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('It worked!');
} else {
reject(new Error('Something failed'));
}
}, 1000);
});
// Using the promise
promise
.then(result => console.log(result)) // 'It worked!'
.catch(error => console.error(error));
fs with Promises
Node.js has promise-based versions of fs functions:
// Using fs/promises (Node.js 14+)
const fs = require('fs').promises;
fs.readFile('hello.txt', 'utf8')
.then(data => {
console.log('File contents:', data);
})
.catch(err => {
console.error('Error:', err.message);
});
Enter async/await
async/await is syntactic sugar for Promises. Same thing, cleaner syntax:
// async-read.js
const fs = require('fs').promises;
async function readMyFile() {
try {
const data = await fs.readFile('hello.txt', 'utf8');
console.log('File contents:', data);
} catch (err) {
console.error('Error:', err.message);
}
}
readMyFile();
Key points:
asyncmarks a function as asynchronousawaitpauses until the Promise resolves- Use
try/catchfor error handling
Before and After
Callbacks:fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
const config = JSON.parse(data);
console.log(config.name);
});
async/await:
async function loadConfig() {
try {
const data = await fs.readFile('config.json', 'utf8');
const config = JSON.parse(data);
console.log(config.name);
} catch (err) {
console.error(err);
}
}
The async version reads like synchronous code but doesn't block!
Sequential vs Parallel
Sequential (one after another):async function sequential() {
const file1 = await fs.readFile('a.txt', 'utf8');
const file2 = await fs.readFile('b.txt', 'utf8');
const file3 = await fs.readFile('c.txt', 'utf8');
// Total time: file1 + file2 + file3
return [file1, file2, file3];
}
Parallel (all at once):
async function parallel() {
const [file1, file2, file3] = await Promise.all([
fs.readFile('a.txt', 'utf8'),
fs.readFile('b.txt', 'utf8'),
fs.readFile('c.txt', 'utf8')
]);
// Total time: max(file1, file2, file3)
return [file1, file2, file3];
}
Use Promise.all when operations don't depend on each other!
Real Example: Fetch User Data
// user-service.js
// Simulate async database calls
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getUser(id) {
await delay(100); // Simulate DB lookup
return { id, name: 'Alice', email: 'alice@example.com' };
}
async function getPosts(userId) {
await delay(100);
return [
{ id: 1, title: 'First Post' },
{ id: 2, title: 'Second Post' }
];
}
async function getComments(postId) {
await delay(50);
return [
{ id: 1, text: 'Great post!' },
{ id: 2, text: 'Thanks for sharing' }
];
}
// Combine them all
async function getUserDashboard(userId) {
try {
// Get user first
const user = await getUser(userId);
console.log('Got user:', user.name);
// Get posts (depends on user)
const posts = await getPosts(user.id);
console.log('Got', posts.length, 'posts');
// Get comments for all posts in parallel
const allComments = await Promise.all(
posts.map(post => getComments(post.id))
);
console.log('Got comments for all posts');
return {
user,
posts: posts.map((post, i) => ({
...post,
comments: allComments[i]
}))
};
} catch (err) {
console.error('Dashboard error:', err.message);
throw err;
}
}
// Run it
getUserDashboard(1).then(dashboard => {
console.log('\nDashboard:', JSON.stringify(dashboard, null, 2));
});
Output:
Got user: Alice
Got 2 posts
Got comments for all posts
Dashboard: {
"user": { "id": 1, "name": "Alice", "email": "alice@example.com" },
"posts": [
{
"id": 1,
"title": "First Post",
"comments": [...]
},
...
]
}
Error Handling Patterns
Try/catch block:async function fetchData() {
try {
const data = await riskyOperation();
return data;
} catch (err) {
console.error('Operation failed:', err.message);
return null; // or throw err to propagate
}
}
Handle specific errors:
async function readConfig() {
try {
const data = await fs.readFile('config.json', 'utf8');
return JSON.parse(data);
} catch (err) {
if (err.code === 'ENOENT') {
console.log('Config not found, using defaults');
return { port: 3000 };
}
throw err; // Re-throw other errors
}
}
Practice Async Patterns
Build real Node.js applications with async/await on cloud workspaces. Pre-configured environments with databases included.
Try It Yourself
Create a file processor that reads, transforms, and writes data:
// processor.js
const fs = require('fs').promises;
async function processFile(inputPath, outputPath) {
try {
// Read input
console.log('Reading:', inputPath);
const input = await fs.readFile(inputPath, 'utf8');
// Transform (uppercase and add line numbers)
const lines = input.split('\n');
const output = lines
.map((line, i) => `${i + 1}: ${line.toUpperCase()}`)
.join('\n');
// Write output
console.log('Writing:', outputPath);
await fs.writeFile(outputPath, output);
console.log('Done! Processed', lines.length, 'lines');
} catch (err) {
if (err.code === 'ENOENT') {
console.error('File not found:', inputPath);
} else {
console.error('Error:', err.message);
}
}
}
// Run it
const input = process.argv[2] || 'input.txt';
const output = process.argv[3] || 'output.txt';
processFile(input, output);
Run it:
echo "hello world\nthis is a test" > input.txt
node processor.js input.txt result.txt
cat result.txt
Output:
Reading: input.txt
Writing: result.txt
Done! Processed 2 lines
result.txt:
1: HELLO WORLD
2: THIS IS A TEST
Key Takeaways
- Promises represent future values
- async marks functions that return Promises
- await pauses until a Promise resolves
- try/catch handles errors in async functions
- Promise.all runs multiple operations in parallel
- fs.promises gives you promise-based file operations
Next, let's build an HTTP server!