JavaScript Async Programming Explained — Promises, Async/Await & Fetch
JavaScript is single-threaded — it can only do one thing at a time — yet web apps constantly wait for network requests, file reads, timers, and user input. Asynchronous programming is how JavaScript handles these waits without freezing the page.
What You’ll Learn
- Why JavaScript needs async programming (the event loop)
- Callbacks and the “callback hell” problem
- Promises — creating, consuming, and chaining
- Async/await — writing async code that reads like synchronous code
- Fetch API for HTTP requests
- Parallel vs sequential execution with Promise.all
Why Async Matters
Without async, clicking a button that fetches data would freeze the entire page until the response arrives. Imagine pressing “Download” in the Doda Browser and the whole browser freezes until the file finishes. Async lets the browser stay responsive while waiting. The Durga Antivirus Pro dashboard uses async to fetch threat updates, scan files in the background, and update the UI without blocking — all while you continue browsing.
Learning Path
sequenceDiagram
participant Main as Main Thread
participant Task as Task Queue
participant API as Web API
Main->>API: fetch(url)
Main->>Main: Continue executing
API->>Task: Response ready
Task->>Main: Process callback
Note over Main: Callbacks → Promises → Async/Await
The Problem: Synchronous Code Blocks
console.log('Start');
const data = fetchData(); // Imaginary — takes 2 seconds
console.log('End');
console.log(data); // We wait 2 seconds to see this!
In a synchronous world, fetchData() blocks everything for 2 seconds. The page freezes, buttons don’t respond, animations stop. Async solves this.
Callbacks (The Original Pattern)
A callback is a function passed as an argument to another function, to be executed later:
function fetchData(callback) {
setTimeout(() => {
callback('Data received');
}, 1000);
}
fetchData((result) => {
console.log(result); // 'Data received' (after 1 second)
});The problem — Callback Hell: When you need multiple sequential operations, callbacks nest endlessly:
fetchUser(1, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
console.log(comments); // Deeply nested — hard to read
});
});
});This is called “callback hell” — code that grows to the right instead of downward.
Promises — A Better Pattern
A Promise represents a value that might not be available yet — like an IOU note. It’s either:
- pending — waiting for the result
- fulfilled — the operation succeeded
- rejected — the operation failed
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('Operation completed'); // Fulfill the promise
} else {
reject('Operation failed'); // Reject the promise
}
});
promise
.then(result => console.log(result)) // 'Operation completed'
.catch(error => console.error(error)) // Handle rejection
.finally(() => console.log('Done')); // Always runs
Analogy: A Promise is like ordering a pizza. You get a receipt (the Promise object). The pizza might be delivered (fulfilled) or you might get a call saying they’re out of dough (rejected). In either case, you’ll be notified.
Promise Chaining
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments))
.catch(err => console.error('Any error in the chain', err));Each .then() returns a new Promise, allowing you to chain operations in a flat, readable structure — no more callback hell.
Static Promise Methods
const p1 = fetch('/api/users');
const p2 = fetch('/api/posts');
Promise.all([p1, p2]) // All must resolve (fast fail)
Promise.allSettled([p1, p2]) // All settle — never rejects
Promise.race([p1, p2]) // First to settle wins
Promise.any([p1, p2]) // First to fulfill wins
When to use each:
Promise.all— Multiple independent requests, need all resultsPromise.allSettled— Need to know the result of each, even failuresPromise.race— Timeout implementation (fetch vs timeout timer)Promise.any— Need the first successful result from multiple sources
Async/Await — Syntactic Sugar
async/await is syntactic sugar over Promises — it makes async code look synchronous:
async function loadData() {
try {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
console.log(posts);
} catch (error) {
console.error('Failed to load:', error);
}
}
loadData();Line by line:
async function— This function now returns a Promise (even if you don’t create one).await fetchUser(1)— Pause execution here until the Promise resolves. The result is assigned touser.try/catch— If anyawaited Promise rejects, execution jumps tocatch.loadData()— Call the async function. It runs immediately and returns a Promise.
Anticipating confusion: await does NOT block the entire program. It only pauses the current async function. Other code (like UI updates) continues running. Think of it like a pit stop in a race — your car pauses, but other cars keep going.
Sequential vs Parallel
// Sequential — slow (2 seconds total)
const a = await fetch('/api/a');
const b = await fetch('/api/b');
// Parallel — fast (1 second total)
const [a, b] = await Promise.all([
fetch('/api/a'),
fetch('/api/b')
]);Use Promise.all for independent requests. Use separate await statements when the second request depends on the first.
Fetch API
The modern way to make HTTP requests:
// GET request
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
return [];
}
}
// POST request
async function createUser(user) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
return response.json();
}Critical: Always check response.ok. Fetch only rejects on network errors, not HTTP errors (like 404 or 500). You must check response.ok or response.status yourself.
Common Mistakes
1. Forgetting await inside async functions
async function load() {
const data = fetch('/api/data'); // Returns Promise, not data!
console.log(data); // Promise { <pending> }
}Fix: Always use await to resolve Promises.
2. Not catching promise rejections
async function load() {
const data = await fetch('/api/data');
// If fetch fails, this throws an unhandled rejection
}Fix: Wrap in try/catch or chain .catch().
3. Sequential promises that should be parallel
const a = await fetch('/api/a');
const b = await fetch('/api/b'); // Waits for a to finish first
Fix: Use Promise.all for independent requests.
4. Not checking response.ok with fetch
const res = await fetch('/api/data');
const data = await res.json(); // Throws if response is not JSON
Fix: Check res.ok before parsing.
5. Mixing .then and await in the same function
Pick one style. Using both is confusing and error-prone.
Practice Questions
What’s the difference between
Promise.allandPromise.allSettled?Promise.allrejects fast (if any rejects).Promise.allSettledwaits for all to settle and never rejects.What does
awaitdo in an async function? It pauses execution until the Promise resolves, then returns the resolved value.How does the event loop handle async operations? Synchronous code runs first, then microtasks (Promises), then macrotasks (setTimeout, I/O).
Why should you check
response.okwith fetch? Fetch only rejects on network errors, not HTTP error status codes.
Challenge: Build a function fetchWithTimeout(url, ms) that fetches a URL but rejects if the request takes longer than ms milliseconds. Use Promise.race with a timeout promise.
FAQ
Try It Yourself
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Async Playground</title>
<style>
body { font-family: Arial, sans-serif; padding: 2rem; }
pre { background: #f4f4f4; padding: 1rem; border-radius: 5px; }
button { margin-right: 0.5rem; padding: 0.5rem 1rem; }
</style>
</head>
<body>
<h1>Async JavaScript Sandbox</h1>
<button id="btnLoad">Simulate API Call</button>
<button id="btnParallel">Parallel Calls</button>
<pre id="output">Click a button to see async in action.</pre>
<script>
const out = document.getElementById('output');
function delay(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
document.getElementById('btnLoad').addEventListener('click', async () => {
out.textContent = 'Loading...';
const result = await delay(1500, 'Data loaded successfully!');
out.textContent = result;
});
document.getElementById('btnParallel').addEventListener('click', async () => {
out.textContent = 'Fetching in parallel...\n';
const start = Date.now();
const [a, b] = await Promise.all([
delay(1000, 'Result A'),
delay(1500, 'Result B')
]);
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
out.textContent += `${a}\n${b}\nCompleted in ${elapsed}s (not 2.5s!)`;
});
</script>
</body>
</html>What’s Next
Now that you understand async JavaScript, learn about modules and advanced topics:
| Lesson | Description |
|---|---|
| JavaScript Home | Back to the JavaScript hub |
| https://tutorials.dodatech.com/programming-languages/javascript/js-modules/ | Modules & Error Handling |
| https://tutorials.dodatech.com/programming-languages/javascript/js-json/ | JSON — parse API responses |
| https://tutorials.dodatech.com/programming-languages/javascript/js-advanced/ | Advanced JavaScript |
| Node.js Async | Server-side async with Node.js |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
What’s Next
Congratulations on completing this Js Async tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro