Modern web applications often perform tasks that take time: fetching data from a server, waiting for user actions, or accessing hardware like cameras or files. These tasks must not block the user interface. JavaScript handles this with asynchronous programming, allowing long-running tasks to happen βin the backgroundβ while the program continues to run.
In this sixth entry of the JavaScript Essentials series, weβll explore three fundamental concepts of asynchronous JavaScript:
- Callbacks
- Promises
- Async/Await
1. What is Asynchronous Programming?
JavaScript is single-threaded, meaning it processes one command at a time in a single sequence. But with asynchronous programming, JavaScript can:
- Start a task (like loading data)
- Continue doing other things
- Handle the task’s result when itβs ready
This is crucial for responsive websites and apps.
2. The JavaScript Event Loop (Simplified)
The event loop is a core part of the JavaScript engine.
- Call Stack β Executes functions line by line.
- Web APIs β Browser-handled tasks like
setTimeout
,fetch
, etc. - Callback Queue β Stores callbacks from Web APIs to run after the stack is empty.
Example:
console.log("Start");
setTimeout(() => {
console.log("Timeout done");
}, 0);
console.log("End");
Output:
Start
End
Timeout done
The setTimeout
callback is delayed until the main thread is free.
3. Callbacks
A callback is a function passed to another function to be executed later.
function fetchData(callback) {
setTimeout(() => {
console.log("Data fetched");
callback();
}, 1000);
}
fetchData(() => {
console.log("Processing data...");
});
π₯ Callback Hell
When many callbacks are nested:
login(user, function() {
getProfile(user, function(profile) {
getPosts(profile.id, function(posts) {
display(posts);
});
});
});
This is hard to read, maintain, and debug β known as βcallback hell.β
4. Promises
A Promise represents the result of an asynchronous operation.
States:
pending
β not yet resolved or rejectedfulfilled
β resolved successfullyrejected
β operation failed
Creating a Promise:
const promise = new Promise((resolve, reject) => {
let success = true;
if (success) resolve("Success!");
else reject("Failure");
});
Using a Promise:
promise
.then(result => console.log(result))
.catch(error => console.error(error));
5. Chaining Promises
You can chain .then()
calls for multiple asynchronous operations.
getUser()
.then(user => getProfile(user.id))
.then(profile => getPosts(profile.id))
.then(posts => display(posts))
.catch(error => console.error("Error:", error));
Each .then()
waits for the previous one to finish.
6. Fetching Data with Promises
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Fetch error:", error));
7. Async/Await β Modern and Clean
async
functions return a promise.await
pauses the execution until the promise resolves.
async function getData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
getData();
β Benefits of Async/Await
- Cleaner, linear syntax
- Easier to read
- Simplifies error handling using
try...catch
8. Running Async Tasks in Parallel
async function loadBoth() {
const [user, profile] = await Promise.all([
fetchUser(),
fetchProfile()
]);
console.log(user, profile);
}
9. Delaying Execution with Promises
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function greetLater() {
await delay(1000);
console.log("Hello after 1 second");
}
10. Handling Errors Gracefully
async function fetchData() {
try {
let res = await fetch("/api/data");
if (!res.ok) throw new Error("Server error");
let data = await res.json();
return data;
} catch (e) {
console.error("Failed:", e);
}
}
11. Using .finally()
fetch("/data")
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => console.log("Done loading"));
12. Async Iteration with for await...of
async function fetchMultiple() {
const urls = ["/data1", "/data2"];
for await (const url of urls.map(fetch)) {
const res = await url.json();
console.log(res);
}
}
13. Practical Example: Submitting a Form
form.addEventListener("submit", async (e) => {
e.preventDefault();
try {
const response = await fetch("/submit", {
method: "POST",
body: new FormData(form),
});
const result = await response.json();
alert("Form submitted!");
} catch {
alert("Failed to submit form.");
}
});
14. Common Pitfalls to Avoid
β Forgetting await
β Nesting await
inside loops needlessly
β Ignoring error handling
β Mixing callbacks and promises incorrectly
15. Summary Table
| Pattern | Syntax Type | Error Handling | Readability |
|——————|——————–|————————|—————–|\n| Callback | Nested Functions | Manual | β Low |\n| Promise | .then()/.catch()
| Built-in | β
Medium |\n| Async/Await | async
/await
| try...catch
| β
β
High |
Conclusion
Asynchronous JavaScript is essential for building fast, non-blocking applications. Understanding callbacks, promises, and async/await will help you handle data fetching, delays, animations, and user interactions effectively.
In the next blog, weβll dive into JavaScript Modulesβhow to organize and reuse your code cleanly using import
and export
.
Would you like me to begin that one next?