Advanced Async Concepts
- This lesson dives into advanced asynchronous patterns and optimization strategies.
Introduction to Advanced Asynchronous JavaScript
Modern JavaScript applications rely heavily on asynchronous behavior to stay fast, responsive, and scalable.
Asynchronous JavaScript helps:
Prevent UI freezing
Handle API calls efficiently
Manage timers and background tasks
Improve overall performance
In this lesson, we will deeply understand:
Microtasks vs Macrotasks
setTimeout and setInterval
Avoiding Callback Hell
Writing Clean Asynchronous Code
Microtasks vs Macrotasks
Why Task Queues Matter
JavaScript is single-threaded, but it can handle asynchronous operations using:
Web APIs
Task queues
Event Loop
When asynchronous code finishes, its callback is placed into a queue waiting for execution.
There are two main types of queues:
Macrotask Queue
Microtask Queue
What Are Macrotasks ?
Macrotasks are tasks that are executed after the current call stack is empty and after all microtasks are completed.
Examples of macrotasks:
setTimeout
setInterval
setImmediate (Node.js)
UI rendering events
What Are Microtasks ?
Microtasks are tasks that have higher priority than macrotasks.
They are executed:
Immediately after the current synchronous code
Before any macrotask
Examples of microtasks:
Promise.then()
Promise.catch()
Promise.finally()
queueMicrotask()
Execution Priority Order
Call Stack (synchronous code)
Microtask Queue
Macrotask Queue
Rendering (browser)
Microtask vs Macrotask Execution Order
Demonstrates JavaScript event loop order where microtasks run before macrotasks.
console.log("Start");
setTimeout(() => {
console.log("Macrotask");
}, 0);
Promise.resolve().then(() => {
console.log("Microtask");
});
console.log("End");
Explanation
Synchronous code runs first
Promise callback (microtask) runs next
setTimeout callback (macrotask) runs last
Why Microtasks Have Higher Priority
Microtasks ensure:
Promise chains complete properly
Consistent async behavior
Reliable state updates before rendering
setTimeout & setInterval
setTimeout()
What It Does
Executes a function once after a specified delay.
Syntax
setTimeout(function, delayInMilliseconds);
Delayed Execution with setTimeout
Runs a function after a specified delay using setTimeout.
setTimeout(() => {
console.log("This runs after 2 seconds");
}, 2000);
Important Notes About setTimeout
Delay is minimum, not guaranteed exact time
Callback runs only when call stack is empty
It is a macrotask
Clearing a setTimeout Timer
Cancels a scheduled timeout using clearTimeout before it executes.
let timerId = setTimeout(() => {
console.log("Hello");
}, 3000);
clearTimeout(timerId);
setInterval()
What It Does
Executes a function repeatedly at a fixed time interval.
Syntax
setInterval(function, intervalInMilliseconds);
Repeated Execution with setInterval
Runs a task repeatedly at fixed intervals and stops it using clearInterval.
let count = 1;
let intervalId = setInterval(() => {
console.log("Count:", count);
count++;
if (count > 5) {
clearInterval(intervalId);
}
}, 1000);
Difference Between setTimeout and setInterval
Avoiding Callback Hell
What Is Callback Hell ?
Callback Hell occurs when:
Callbacks are nested inside callbacks
Code becomes deeply indented
Logic becomes hard to read and maintain
Callback Hell in JavaScript
Shows deeply nested callbacks that make code hard to read and maintain.
loginUser(user, function () {
getProfile(function () {
getOrders(function () {
getPayments(function () {
console.log("All data loaded");
});
});
});
});
Problems:
Poor readability
Difficult debugging
Error handling becomes complex
Why Callback Hell Is Dangerous
Hard to scale code
Difficult error handling
Increased bug probability
Poor maintainability
Solution 1: Named Functions
Avoiding Callback Hell with Named Functions
Uses separate named functions to simplify and organize asynchronous flow.
function loadPayments() {
console.log("Payments loaded");
}
function loadOrders() {
loadPayments();
}
function loadProfile() {
loadOrders();
}
loadProfile();
Improves readability slightly but still limited.
Solution 2: Promises
Avoiding Callback Hell with Promises
Uses Promise chaining to simplify asynchronous code and improve readability.
loginUser()
.then(getProfile)
.then(getOrders)
.then(getPayments)
.then(() => console.log("All data loaded"))
.catch(error => console.error(error));
Much cleaner and readable.
Topic 4: Writing Clean Async Code
Best Practice 1: Use async / await
async/await makes asynchronous code look synchronous.
Avoiding Callback Hell with Async/Await
Uses async/await to write clean and sequential asynchronous code with error handling.
async function loadData() {
try {
await loginUser();
await getProfile();
await getOrders();
await getPayments();
console.log("All data loaded");
} catch (error) {
console.error(error);
}
}
loadData();
Best Practice 2: Always Handle Errors
Never ignore errors in async code.
Bad:
fetchData();
Good:
try {
await fetchData();
} catch (err) {
console.error(err);
}
Best Practice 3: Avoid Unnecessary await
Bad:
await console.log("Hello");
Good:
console.log("Hello");
Only await promises.
Best Practice 4: Run Independent Tasks in Parallel
let [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
Improves performance significantly.
Best Practice 5: Keep Async Functions Small
One responsibility per function
Easy testing
Better reuse
Common Beginner Mistakes
Mixing callbacks with promises
Forgetting await
Not using try...catch
Blocking the main thread
Overusing setTimeout
Real-World Use Cases
API calls
Authentication
Background syncing
Timers and polling
Animations and transitions