JavaScript Async Programming | Promises and async/await Explained
이 글의 핵심
Hands-on async JavaScript: escape callback hell with Promises, write readable code with async/await, and compose concurrent operations safely.
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