Asynchronous JavaScript - Callbacks, Promises, and Async Await
Synchronous vs Asynchronous
JavaScript is single-threaded, meaning it executes one operation at a time in order. This is called synchronous execution:
console.log("Step 1");
console.log("Step 2");
console.log("Step 3");
// Output: Step 1, Step 2, Step 3 — always in order
Some operations take time — network requests, reading files, timers. If JavaScript waited for these to finish before continuing, the browser would freeze. Asynchronous code lets JavaScript start an operation and continue running other code while waiting for the result.
Callbacks
A callback is a function passed as an argument to another function, to be called when an operation completes:
function fetchUser(userId, callback) {
// Simulate a 1-second network delay
setTimeout(() => {
const user = { id: userId, name: "Amara" };
callback(null, user); // convention: (error, result)
}, 1000);
}
fetchUser(1, (error, user) => {
if (error) {
console.log("Error:", error);
return;
}
console.log("User:", user.name);
});
console.log("This runs before the user arrives!"); // Runs immediately
Callback Hell
When you need to chain multiple async operations, callbacks nest deeply — a problem called callback hell:
getUser(id, (err, user) => {
getCourses(user.id, (err, courses) => {
getProgress(courses[0].id, (err, progress) => {
renderDashboard(user, courses, progress, (err, html) => {
// Hard to read, hard to maintain
});
});
});
});
Promises and async/await were created to solve this problem.
Promises
A Promise represents a value that will be available in the future. It can be in one of three states:
- Pending — the operation has not finished yet
- Fulfilled — the operation succeeded, a value is available
- Rejected — the operation failed, an error is available
function fetchUserPromise(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: "Amara" }); // Success
} else {
reject(new Error("Invalid user ID")); // Failure
}
}, 1000);
});
}
// Using .then() and .catch()
fetchUserPromise(1)
.then(user => {
console.log("User:", user.name);
return user.id; // Pass value to next .then()
})
.then(id => {
console.log("Fetching courses for user", id);
})
.catch(error => {
console.error("Something went wrong:", error.message);
})
.finally(() => {
console.log("Request complete"); // Always runs
});
Async / Await
Async/await is syntactic sugar built on top of Promises. It makes asynchronous code look and read like synchronous code.
async function loadDashboard(userId) {
try {
const user = await fetchUserPromise(userId);
console.log("Loaded user:", user.name);
const courses = await fetchCourses(user.id);
console.log("Loaded courses:", courses.length);
return { user, courses };
} catch (error) {
console.error("Dashboard error:", error.message);
}
}
loadDashboard(1);
Key rules:
- A function must be declared with the
asynckeyword to useawaitinside it awaitpauses execution of the async function until the Promise resolves- Wrap
awaitcalls in atry/catchblock to handle errors
Awaiting Multiple Promises in Parallel
If your promises are independent, run them in parallel with Promise.all:
async function loadAll(userId) {
try {
// These two requests happen simultaneously
const [user, announcements] = await Promise.all([
fetchUserPromise(userId),
fetchAnnouncements()
]);
console.log(user.name, announcements);
} catch (error) {
console.error(error);
}
}
Error Handling
Always handle errors in async code. Unhandled Promise rejections will crash Node.js applications and show warnings in browsers:
// With .catch()
fetchUserPromise(-1).catch(err => console.error(err.message));
// With async/await + try/catch (preferred)
async function safe() {
try {
const result = await fetchUserPromise(-1);
} catch (err) {
console.error("Caught:", err.message);
}
}
Key Takeaways
- JavaScript is single-threaded; asynchronous code allows operations like network requests to run without blocking
- Callbacks were the original async pattern but lead to deeply nested "callback hell" with complex chains
- Promises represent a future value and have three states: pending, fulfilled, and rejected
- .then() handles success, .catch() handles errors, and .finally() always runs
- async/await makes Promise-based code readable by letting you write it in a linear, top-to-bottom style
- Use Promise.all() to run multiple independent async operations in parallel for better performance
Practice Exercise
- Create a function
delay(ms)that returns a Promise which resolves aftermsmilliseconds usingsetTimeout - Write an async function
runSequence()that usesawait delay()to log "Start", wait 1 second, log "Middle", wait 1 second, then log "End" - Rewrite the function to use
Promise.allwith two delay calls running at the same time and observe the timing difference
Try it yourself
Key Takeaways
- JavaScript is single-threaded; async code allows long operations to complete without freezing the browser
- Callbacks were the first async pattern but lead to deeply nested code (callback hell) when chaining operations
- A Promise is an object representing a future value; it is pending, fulfilled, or rejected
- .then() handles success, .catch() handles errors, and .finally() always runs after a Promise settles
- async/await is syntactic sugar over Promises that makes async code read like synchronous code
- Promise.all([p1, p2]) runs independent Promises in parallel, resolving when all complete
Quick Quiz
1.What is callback hell?
2.What are the three possible states of a Promise?
3.What must you add to a function before you can use await inside it?
4.What does Promise.all([p1, p2]) do?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx