Node.js: Promises and Async/Await
Last updated: 2026-08-26
1. What You'll Learn
- Core Methods and Use Cases of the fs.promises API
- Behavioral Differences and Choices Between Promise.all, race, allSettled, and any
- Error handling with async/await and try/catch
- Concurrency Control: Limiting the value of asynchronous tasks running simultaneously
- Use Cases for Sequential Execution, Parallel Execution, and Limited Concurrency
- Best Practices for Chained Promise Calls
- util.promisify converts callback-style code to Promises
2. Story: Charlie's Bulk Data Download
Charlie was responsible for downloading configuration data from 100 external APIs. At first, he used for to make requests one by one in a loop; each request took about 30 seconds, so it took 50 minutes to complete all 100. Later, he switched to Promise.all to make parallel requests, reducing the total time to 30 seconds—but the server immediately returned 429 Too Many Requests. Charlie realized he needed to limit the value of concurrent requests and ultimately capped it at 5, bringing the total time down to about 10 minutes—a solution that was both efficient and didn’t trigger rate limiting.
This story highlights the core trade-off in asynchronous programming: speed vs. resources. Promises and async/await are the tools for managing this balance.
3. fs.promises API
Starting with version 10, Node.js provides fs.promises, which wraps file system operations into methods that return Promises, putting an end to callback hell.
(1) fs callbacks vs fs.promises vs promisify
| Feature | fs callbacks | fs.promises | util.promisify(fs.xxx) |
|---|---|---|---|
| Return value | void; the result is passed via a callback | Promise | Promise |
| Error Handling | The first parameter of the callback, err |
try/catch or .catch() |
try/catch or .catch() |
| Code Style | Nested Callbacks | Flat async/await | Flat async/await |
| Available Versions | All Versions | v10+ | v8+ |
| Typical Usage | fs.readFile(path, (err, data) => {}) |
await fs.promises.readFile(path) |
const readFile = promisify(fs.readFile); await readFile(path) |
▶ Example: Reading and Writing Files with fs.promises
const fs = require('fs').promises;
async function readAndWrite() {
try {
const data = await fs.readFile('input.txt', 'utf8');
const upper = data.toUpperCase();
await fs.writeFile('output.txt', upper);
console.log('Done');
} catch (err) {
console.error('Error:', err.message);
}
}
readAndWrite();
▶ Example: Directory operations with fs.promises
const fs = require('fs').promises;
async function listFiles(dir) {
try {
await fs.mkdir(dir, { recursive: true });
const files = await fs.readdir(dir);
for (const file of files) {
const stat = await fs.stat(`${dir}/${file}`);
console.log(`${file} - ${stat.size} bytes`);
}
} catch (err) {
console.error(err.message);
}
}
listFiles('./my-dir');
▶ Example: util.promisify converts callback functions
const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
async function copy() {
const data = await readFile('source.txt', 'utf8');
await writeFile('dest.txt', data);
console.log('Copied');
}
copy();
4. Promise Chaining Methods
Four static methods determine how multiple Promises interact with each other; choosing the wrong combination of methods can lead to vastly different results.
(1) Comparison of Four Combination Methods
| Method | When all operations succeed | When there is a failure | Return Value | Typical Uses |
|---|---|---|---|---|
Promise.all |
Return the entire result array | Reject upon the first failure; reject the entire set | Result array | All tasks must be completed |
Promise.race |
Return the first completed result | Reject upon first failure | Single value | Timeout control, fastest response |
Promise.allSettled |
Return all results | Do not reject; include failure messages | {status, value/reason}[] |
Return all results, regardless of success or failure |
Promise.any |
Return the first successful result | Reject only if all fail (AggregateError) | Single value | Multi-source race, take the fastest success |
▶ Example: Promise.all — Passes only if all promises succeed
async function fetchAll() {
const urls = [
'https://api.example.com/a',
'https://api.example.com/b',
'https://api.example.com/c',
];
try {
const results = await Promise.all(
urls.map(url => fetch(url).then(r => r.json()))
);
console.log('All succeeded:', results.length);
} catch (err) {
console.error('One failed:', err.message);
}
}
▶ Example: Promise.allSettled — Never Rejects
async function fetchAllSettled() {
const tasks = [
Promise.resolve({ id: 1 }),
Promise.reject(new Error('Server down')),
Promise.resolve({ id: 3 }),
];
const results = await Promise.allSettled(tasks);
const succeeded = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
console.log(`Succeeded: ${succeeded.length}, Failed: ${failed.length}`);
failed.forEach(r => console.error('Reason:', r.reason.message));
}
▶ Example: Promise.race — Timeout Control
function fetchWithTimeout(url, ms) {
const fetchTask = fetch(url).then(r => r.json());
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([fetchTask, timeout]);
}
async function demo() {
try {
const data = await fetchWithTimeout('https://api.example.com/slow', 3000);
console.log(data);
} catch (err) {
console.error(err.message);
}
}
▶ Example: Promise.any — Retrieving a Success Result from Multiple Sources in a Race Condition
async function fastestMirror() {
const mirrors = [
fetch('https://mirror1.example.com/data').then(r => r.json()),
fetch('https://mirror2.example.com/data').then(r => r.json()),
fetch('https://mirror3.example.com/data').then(r => r.json()),
];
try {
const result = await Promise.any(mirrors);
console.log('Fastest response:', result);
} catch (err) {
console.error('All mirrors failed:', err.errors.length);
}
}
▶ Example: (2) Mermaid: Comparison of Promise Chaining Method Execution
flowchart TB
subgraph all["Promise.all"]
A1["Task A ✅"] --- A2["Task B ✅"] --- A3["Task C ✅"]
AR["→ [A, B, C] ✅"]
end
subgraph race["Promise.race"]
R1["Task A ⏱ 1s"] --- R2["Task B ⏱ 3s"] --- R3["Task C ⏱ 2s"]
RR["→ A ✅ (Fastest)"]
end
subgraph settled["Promise.allSettled"]
S1["Task A ✅"] --- S2["Task B ❌"] --- S3["Task C ✅"]
SR["→ [{fulfilled:A}, {rejected:B}, {fulfilled:C}] ✅"]
end
subgraph any["Promise.any"]
N1["Task A ❌"] --- N2["Task B ✅ ⏱ 2s"] --- N3["Task C ✅ ⏱ 3s"]
NR["→ B ✅ (Success as Quickly as Possible)"]
end
all --> AR
race --> RR
settled --> SR
any --> NR
style AR fill:#c8e6c9
style RR fill:#c8e6c9
style SR fill:#fff9c4
style NR fill:#c8e6c9
5. Error Handling with async/await
async/await makes asynchronous code look like synchronous code, and error handling follows suit try/catch—but there are a few pitfalls.
(1) Comparison of Error Handling Models
| Pattern | Implementation | Advantages | Disadvantages |
|---|---|---|---|
| Callback | if (err) { handle } |
Simple and intuitive | Deep nesting, easy to overlook |
| Promise.catch() | promise.then().catch() |
Chained, reusable | Still complex when nested |
| async/await + try/catch | try { await } catch {} |
Synchronous style, good readability | Each await must be wrapped |
| Wrapper Function | const [err, data] = await to(promise) |
No try/catch, concise | Requires importing helper functions |
▶ Example: Wrapping an async function with try/catch
const fs = require('fs').promises;
async function safeReadFile(path) {
try {
const data = await fs.readFile(path, 'utf8');
return { ok: true, data };
} catch (err) {
return { ok: false, error: err.message };
}
}
async function main() {
const result = await safeReadFile('missing.txt');
if (!result.ok) {
console.error('Failed:', result.error);
return;
}
console.log('Content:', result.data);
}
main();
▶ Example: Error-wrapping function without try/catch
function to(promise) {
return promise
.then(data => [null, data])
.catch(err => [err, null]);
}
async function main() {
const fs = require('fs').promises;
const [err, data] = await to(fs.readFile('config.json', 'utf8'));
if (err) {
console.error('Read failed:', err.message);
return;
}
console.log('Config:', data);
}
main();
6. Concurrency Control
(1) Sequential vs. Parallel vs. Constrained Concurrency
| Execution Method | Total Time (N tasks, each taking T time) | Advantages | Disadvantages | Suitable Scenarios |
|---|---|---|---|---|
| Sequential execution | N × T | Simple, resource-efficient | Slow | Tasks with dependencies |
| Fully parallel | ≈ T | Fastest | High peak resource usage; may be throttled | Few independent tasks |
| Concurrency-limited | ≈ N/concurrency × T | Balances speed and resources | Slightly more complex to implement | Large value of independent tasks, API rate limiting |
▶ Example: Sequential Execution
const fs = require('fs').promises;
async function sequential() {
const files = ['a.txt', 'b.txt', 'c.txt'];
const results = [];
for (const file of files) {
const data = await fs.readFile(file, 'utf8');
results.push(data);
}
console.log('Results:', results.length);
}
▶ Example: Constrained Concurrency Control Function
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = task().then(result => {
executing.delete(p);
return result;
});
executing.add(p);
results.push(p);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
▶ Example: Using the Limited Concurrent Downloads API
async function fetchApi(url) {
const res = await fetch(url);
return res.json();
}
async function batchFetch() {
const urls = Array.from({ length: 100 }, (_, i) =>
`https://api.example.com/item/${i + 1}`
);
const tasks = urls.map(url => () => fetchApi(url));
const results = await limitConcurrency(tasks, 5);
console.log(`Fetched ${results.length} items`);
}
7. Best Practices for Chaining Promises
(1) Rules for Chain Calls
| Rule | Description | Counterexample |
|---|---|---|
| Always return a Promise | Ensure chaining | promise.then(() => { doSomething() }) No return |
| Always handle errors | Add .catch() at the end | A chained block without .catch() results in uncaught errors |
| Avoid Nesting | Flatten .then() Chains | .then(() => { return p.then(...) }) |
| Replace long chains with async/await | Replace more than 3 .then() calls with await | More than 5 levels of nested .then() calls |
| Note: A throw in .then() | will be caught by the next .catch() | because I thought a throw wouldn't interrupt the flow |
▶ Example: Flat Chain Call
const fs = require('fs').promises;
function processFile(path) {
return fs.readFile(path, 'utf8')
.then(data => data.trim())
.then(data => data.toUpperCase())
.then(data => fs.writeFile('output.txt', data))
.then(() => console.log('Saved'))
.catch(err => console.error('Error:', err.message));
}
processFile('input.txt');
8. Comprehensive Example: A Concurrency-Limited Batch File Processing Tool
Build a tool: Read a directory → Limit to 3 concurrent file processes → Collect results → Output statistics.
const fs = require('fs').promises;
const path = require('path');
async function processFile(filePath) {
const stat = await fs.stat(filePath);
const content = await fs.readFile(filePath, 'utf8');
const lines = content.split('\n').length;
const words = content.split(/\s+/).filter(Boolean).length;
return {
file: path.basename(filePath),
size: stat.size,
lines,
words,
};
}
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = task().then(result => {
executing.delete(p);
return result;
});
executing.add(p);
results.push(p);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
async function batchProcessDir(dirPath, concurrency = 3) {
console.log(`Scanning directory: ${dirPath}`);
const files = await fs.readdir(dirPath);
const filePaths = files
.filter(f => f.endsWith('.txt') || f.endsWith('.md') || f.endsWith('.json'))
.map(f => path.join(dirPath, f));
if (filePaths.length === 0) {
console.log('No matching files found.');
return;
}
const tasks = filePaths.map(fp => () => processFile(fp));
const results = await limitConcurrency(tasks, concurrency);
console.log('\n--- File Statistics ---');
console.log('File'.padEnd(20) + 'Size'.padEnd(10) + 'Lines'.padEnd(8) + 'Words');
console.log('-'.repeat(46));
let totalLines = 0;
let totalWords = 0;
let totalSize = 0;
for (const r of results) {
console.log(
r.file.padEnd(20) +
String(r.size).padEnd(10) +
String(r.lines).padEnd(8) +
String(r.words)
);
totalLines += r.lines;
totalWords += r.words;
totalSize += r.size;
}
console.log('-'.repeat(46));
console.log(
'TOTAL'.padEnd(20) +
String(totalSize).padEnd(10) +
String(totalLines).padEnd(8) +
String(totalWords)
);
console.log(`\nProcessed ${results.length} files (concurrency: ${concurrency})`);
}
batchProcessDir('./data', 3).catch(err => console.error('Fatal:', err.message));
Performance:
Scanning directory: ./data
--- File Statistics ---
File Size Lines Words
----------------------------------------------
config.json 256 12 42
readme.md 1024 45 312
notes.txt 512 28 178
----------------------------------------------
TOTAL 1792 85 532
Processed 3 files (concurrency: 3)
❓ FAQ
Promise.all?Promise.allSettled when you need all results.limitConcurrency function that uses a Set to track active Promises and Promise.race to control the value of concurrent operations; in production environments, you can also use the p-limit library.await only be used inside async functions?await, which allows await to be used directly at the top level of an ES Module, but in CommonJS modules, it still needs to be wrapped inside an async function.util.promisify convert all callback functions?(err, result) => {} format. Custom multi-argument callbacks must be wrapped manually.Promise.allSettled return results in the same order as the input?async function return?async function always returns a Promise. Even if it returns a regular value, it is automatically wrapped in Promise.resolve(value).📖 Summary
- Key Concepts and How to Apply Them
- Story: The Core Concepts and Usage of Charlie's Batch Data Download
- Core Concepts and Usage of the fs.promises API
- Core Concepts and Usage of Promise Chaining Methods
- Core Concepts and Usage of Error Handling with async/await
- Core Concepts and Usage of Concurrency Control
- Core Concepts and Best Practices for Chaining Promises
- Comprehensive Example: Core Concepts and Usage of a Concurrency-Limited Batch File Processing Tool
📝 Exercises
- Complete all the code examples in this lesson and make sure each one runs correctly.
- Modify the comprehensive example and add your own extensions
- Review the official documentation, identify 1–2 APIs not covered in this lesson, and write test code for them.
- Reflection: How would you apply what you’ve learned in this lesson to a real-world project?
- Try to combine what you’ve learned in this lesson with material from previous lessons to build a small project.