When I first started learning JavaScript, I noticed something confusing. Some lines of code executed immediately, while others ran after a delay.
For example:
console.log("Start");
setTimeout(() => {
console.log("Inside timeout");
}, 2000);
console.log("End");
Output:
Start
End
Inside timeout
Why does this happen if JavaScript executes code from top to bottom?
Let's understand the concepts behind it.
1. Synchronous vs Asynchronous JavaScript
JavaScript executes synchronous code one statement at a time. Each operation must finish before the next one proceeds.
Asynchronous operations, such as timers and network requests, allow JavaScript to continue executing other code while waiting for a result.
2. Call Stack
The call stack keeps track of the functions currently being executed. It follows the LIFO principle: Last In, First Out.
When a function calls another function, the new function is placed on top of the stack and executes first.
3. Runtime Environment and Callback Queue
The browser or Node.js runtime provides features such as timers, networking, and file operations.
When an asynchronous operation finishes, its callback can be scheduled for execution. The callback must wait until JavaScript can run it.
4. Event Loop
The event loop coordinates the execution of scheduled callbacks and other queued work.
A simplified way to understand it is that synchronous code runs first, Promise callbacks are processed at microtask checkpoints, and timer callbacks run when their scheduled tasks are selected.
Consider this example:
console.log("Start");
setTimeout(() => console.log("Timer"), 0);
Promise.resolve().then(() => console.log("Promise"));
console.log("End");
Output:
Start
End
Promise
Timer
Promise callbacks run as microtasks, which are processed before the next timer task in this example.
5. What Are Promises?
A Promise represents the eventual success or failure of an operation. It has three states:
Pending: Still waiting for a result.
Fulfilled: Completed successfully.
Rejected: Failed.
Example:
const promise = new Promise((resolve, reject) => {
resolve("Data received");
});
promise.then((data) => {
console.log(data);
});
Here, resolve() fulfills the Promise, and .then() handles the result.
We can handle errors using .catch():
promise
.then((data) => console.log(data))
.catch((error) => console.error(error));
Promises also support chaining, allowing one asynchronous operation to depend on the result of another.
6. Async/Await
JavaScript provides async and await to make Promise-based code easier to read.
async function getData() {
try {
const response = await fetch("/api/users");
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
An async function returns a Promise, while await suspends that function until the awaited Promise settles. It does not block the entire JavaScript thread.
Conclusion
Asynchronous JavaScript becomes easier to understand when you know how its components work together:
Call Stack: Executes JavaScript functions.
Runtime Environment: Handles capabilities such as timers and networking.
Queues: Hold scheduled work.
Event Loop: Coordinates execution of queued work.
Promises: Represent eventual results or failures.
Async/Await: Makes Promise-based code easier to write and understand.
Understanding these concepts gives you a solid foundation for working with API requests, databases, and other asynchronous operations in JavaScript.
About the author
Building cool stuff in web
Comments
Join the discussionNo comments yet. Be the first to comment.
