JavaScript Promise & async/await Guide | Async Patterns for Node & Browser
이 글의 핵심
Master Promises and async/await: readable asynchronous code, parallel and sequential flows, and robust error handling in modern JavaScript.
Introduction
What is asynchronous programming?
Asynchronous code does not block the rest of the program while waiting for a long-running operation.
Sync vs async:
console.log("1");
console.log("2");
console.log("3");
console.log("1");
setTimeout(() => console.log("2"), 1000);
console.log("3");
Why async matters:
- Network: HTTP calls can take hundreds of ms or more
- Disk: file I/O in Node.js
- Timers:
setTimeout,setInterval - User input: clicks, keyboard
1. Callbacks
Basic callback
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: "Alice" };
callback(data);
}, 1000);
}
console.log("start");
fetchData(data => {
console.log("data:", data);
});
console.log("end");
Callback hell
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
console.log(details);
});
});
});
2. Promises
What is a Promise?
A Promise represents the eventual outcome of an async operation.
States:
- Pending: initial
- Fulfilled: success
- Rejected: failure
Creating a Promise
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) resolve("ok");
else reject("fail");
}, 1000);
});
promise
.then(result => console.log(result))
.catch(err => console.error(err))
.finally(() => console.log("done"));
Chaining
function fetchUser(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: "Alice" });
}, 1000);
});
}
function fetchOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ id: 1, product: "laptop" }]);
}, 1000);
});
}
function fetchOrderDetails(orderId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: orderId, price: 1200 });
}, 1000);
});
}
fetchUser(1)
.then(user => {
console.log("user:", user);
return fetchOrders(user.id);
})
.then(orders => {
console.log("orders:", orders);
return fetchOrderDetails(orders[0].id);
})
.then(details => {
console.log("details:", details);
})
.catch(err => console.error("error:", err))
.finally(() => console.log("all steps finished"));
Rule: return the next Promise from then to continue the chain.
promise
.then(result => {
return anotherPromise();
})
.then(result2 => { });
promise
.then(result => {
anotherPromise();
})
.then(result2 => {
// result2 may be undefined if you forgot return
});
Static methods
Promise.resolve(42).then(console.log);
Promise.reject("err").catch(console.error);
Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])
.then(console.log);
Promise.all([
Promise.resolve(1),
Promise.reject("err"),
Promise.resolve(3)
]).catch(console.error);
Promise.allSettled([
Promise.resolve(1),
Promise.reject("err"),
Promise.resolve(3)
]).then(console.log);
Promise.race([
new Promise(r => setTimeout(() => r(1), 1000)),
new Promise(r => setTimeout(() => r(2), 500)),
]).then(console.log);
Promise.any([
Promise.reject("e1"),
new Promise(r => setTimeout(() => r(2), 500)),
]).then(console.log);
Practical snippets:
async function loadDashboard() {
try {
const [user, orders, notifications] = await Promise.all([
fetchUser(),
fetchOrders(),
fetchNotifications()
]);
renderDashboard(user, orders, notifications);
} catch (error) {
console.error("dashboard load failed:", error);
}
}
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), ms)
);
}
async function fetchWithTimeout(url, ms = 5000) {
return Promise.race([fetch(url), timeout(ms)]);
}
3. async/await
Basics
async function fetchData() {
return "data";
}
fetchData().then(console.log);
async function getData() {
const data = await fetchData();
console.log(data);
}
Sequential flow
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function fetchUser(userId) {
await delay(1000);
return { id: userId, name: "Alice" };
}
async function main() {
try {
const user = await fetchUser(1);
const orders = await fetchOrders(user.id);
const details = await fetchOrderDetails(orders[0].id);
console.log(details);
} catch (error) {
console.error("error:", error);
}
}
Parallelism
async function sequential() {
const u1 = await fetchUser(1);
const u2 = await fetchUser(2);
return [u1, u2];
}
async function parallel() {
const [u1, u2, u3] = await Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
return [u1, u2, u3];
}
async function parallel2() {
const p1 = fetchUser(1);
const p2 = fetchUser(2);
return [await p1, await p2];
}
4. Error handling
try/catch with async/await
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(error.message);
return null;
} finally {
console.log("request finished");
}
}
Promise errors
fetch("https://api.example.com/data")
.then(response => {
if (!response.ok) throw new Error("HTTP error");
return response.json();
})
.then(console.log)
.catch(console.error)
.finally(() => console.log("done"));
5. Practical examples
Fetch GitHub user
async function fetchGitHubUser(username) {
try {
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) {
throw new Error(`User not found: ${response.status}`);
}
const user = await response.json();
return { name: user.name, bio: user.bio, repos: user.public_repos };
} catch (error) {
console.error(error.message);
return null;
}
}
Retry with backoff
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (response.ok) return await response.json();
} catch (error) {
console.log(`attempt ${i + 1} failed`);
if (i === maxRetries - 1) throw new Error("max retries exceeded");
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
}
}
}
Timeout wrapper
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("Timeout")), ms);
});
}
async function fetchWithTimeout(url, ms = 5000) {
try {
const response = await Promise.race([fetch(url), timeout(ms)]);
return await response.json();
} catch (error) {
if (error.message === "Timeout") console.error("request timed out");
throw error;
}
}
Parallel fetch with partial failure
async function fetchMultipleUsers(userIds) {
const promises = userIds.map(id => fetchUser(id));
try {
return await Promise.all(promises);
} catch (error) {
console.error("fetch failed:", error);
return [];
}
}
async function fetchMultipleUsersSettled(userIds) {
const results = await Promise.allSettled(userIds.map(id => fetchUser(id)));
return results
.filter(r => r.status === "fulfilled")
.map(r => r.value);
}
Sequential processing
async function processSequentially(items) {
let result = 0;
for (let item of items) {
result = await processItem(item, result);
}
return result;
}
async function processSequentiallyReduce(items) {
return items.reduce(async (accP, item) => {
const acc = await accP;
return processItem(item, acc);
}, Promise.resolve(0));
}
6. Event loop
JavaScript runs one thread per realm, but the event loop schedules async work.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
Rough order:
- Sync code:
1,4 - Microtasks (Promises):
3 - Macrotasks (
setTimeout):2
setTimeout(() => console.log("macrotask"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("sync");
7. Patterns
Loading state
class DataFetcher {
constructor() {
this.loading = false;
this.data = null;
this.error = null;
}
async fetch(url) {
this.loading = true;
this.error = null;
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
this.data = await response.json();
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
return this.data;
}
}
Simple cache
class CachedFetcher {
constructor() {
this.cache = new Map();
}
async fetch(url) {
if (this.cache.has(url)) return this.cache.get(url);
const response = await fetch(url);
const data = await response.json();
this.cache.set(url, data);
return data;
}
}
Concurrency queue
class TaskQueue {
constructor(concurrency = 1) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
async add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.process();
});
}
async process() {
if (this.running >= this.concurrency || this.queue.length === 0) return;
this.running++;
const { task, resolve, reject } = this.queue.shift();
try {
resolve(await task());
} catch (error) {
reject(error);
} finally {
this.running--;
this.process();
}
}
}
8. Common mistakes
Forgetting await
async function getData() {
const data = fetchData();
console.log(data);
}
async function getDataFixed() {
const data = await fetchData();
console.log(data);
}
Serial vs parallel
async function slow() {
const u1 = await fetchUser(1);
const u2 = await fetchUser(2);
return [u1, u2];
}
async function fast() {
return Promise.all([fetchUser(1), fetchUser(2)]);
}
forEach with await
async function bad(items) {
items.forEach(async item => {
await processItem(item);
});
console.log("done too early");
}
async function goodSequential(items) {
for (const item of items) await processItem(item);
console.log("done");
}
async function goodParallel(items) {
await Promise.all(items.map(processItem));
console.log("done");
}
Missing try/catch
async function getData() {
try {
return await fetchData();
} catch (error) {
console.error(error);
return null;
}
}
9. Exercises
Rewrite chain as async/await
async function getDataAsync() {
try {
const user = await fetchUser(1);
const orders = await fetchOrders(user.id);
return await fetchOrderDetails(orders[0].id);
} catch (error) {
console.error(error);
}
}
Parallel users, sequential orders per user
async function processUsers(userIds) {
const users = await Promise.all(userIds.map(id => fetchUser(id)));
const results = [];
for (const user of users) {
const orders = await fetchOrders(user.id);
results.push({ user, orders });
}
return results;
}
Promisify a callback API
function readFileCallback(filename, callback) {
setTimeout(() => {
callback(null, `contents of ${filename}`);
}, 1000);
}
function readFilePromise(filename) {
return new Promise((resolve, reject) => {
readFileCallback(filename, (error, data) => {
if (error) reject(error);
else resolve(data);
});
});
}
Summary
Key points
- Styles: callbacks (basic), Promises (composition), async/await (readability).
- Static helpers:
Promise.all,Promise.allSettled,Promise.race,Promise.any. - Errors:
.catchon chains;try/catcharoundawait. - Performance: sequential
awaitvsPromise.allfor independence. - Event loop: microtasks before the next macrotask.
Best practices
- Prefer async/await for linear flow.
- Use
Promise.allwhen tasks are independent. - Always handle errors.
- Add timeouts for network calls when appropriate.
- Consider retries with backoff for flaky APIs.
Next steps
- JavaScript DOM
- JavaScript classes
- JavaScript modules
Related posts
- Node.js async programming | callbacks, Promises, async/await
- JavaScript functions | declarations, arrows, callbacks, closures