Javascript executes code in a single thread, line by line, in a deterministic direction.
1const printAfterDelay = (message, delay) => {
2 // simulating a long-running task
3 let iter = 0;
4 while (iter < delay) {
5 iter++;
6 }
7 console.log(message);
8};
9
10// Executes immediately
11console.log('Statement 1'); // Output: Statement 1
12// Executes after a delay
13printAfterDelay('Statement 2', 1000000000); // Output: Statement 2
14// Executes after pervious task was completed
15console.log('Statement 3'); // Output: Statement 3
Diagram Loading...
In the above example, printAfterDelay()
blocks the thread. The statements after it will not execute until the function returns.
The call stack is a data structure that keeps track of function calls. It follows the Last In First Out (LIFO) principle.
1function functionD() {
2 console.log("Function D started");
3}
4const functionC = () => {
5 console.log("Function C started");
6 functionD();
7}
8const functionB = () => {
9 console.log("Function B started");
10 functionC();
11}
12const functionA = () => {
13 console.log("Function A started");
14 functionB();
15}
16
17console.log("Hello World");
18functionA();
19console.log("Bye World");
Diagram Loading...
When you call functionA()
it gets added to the call stack.
functionA()
then calls functionB()
, which is also added to the stack.
Next, functionB()
calls functionC()
, and functionC()
is pushed onto the stack.
functionC()
calls functionD()
, which is then pushed onto the stack.
After functionD()
finishes executing, it is removed from the stack.
This process repeats, with each function returning and being popped off the stack, until the stack is completely cleared.
Once the call stack is empty, JavaScript can move on to execute the next statements in the code
When a function is executing on the call stack, its execution context maintains references to its lexical environment, allowing it to access variables according to its predefined lexical scope.
You can learn more about lexical scope in the following article:
Understanding Closures: Capturing Lexical EnvironmentsWhen a function is invoked, it gets pushed onto the call stack. If the function works with primitive values, those values are directly stored on the call stack. However, when the function deals with reference types like objects or arrays, only a reference to their location in the heap is placed on the call stack, not the actual data itself.
1// Call stack memory
2const genericFunction = (primitiveValue, referenceValue) => {
3 console.log("Primitive Value:", primitiveValue);
4 console.log("Reference Value:", referenceValue);
5};
6
7// gets copied onto the stack
8const primitiveValue = 42;
9// sits on the heap, call stack has a reference to it
10const referenceValue = { name: "John Doe" };
11
12genericFunction(primitiveValue, referenceValue);
Certain time-consuming tasks, such as network requests or timers, can be delegated to background threads. This allows JavaScript to keep running other code on the main thread without waiting for those tasks to finish. This approach is known as asynchronous execution.
1const runAfterTimeout = (callback, delay) => {
2 setTimeout(() => {
3 callback();
4 }, delay);
5};
6
7console.log('Started Script');
8runAfterTimeout(() => {
9 console.log('Timeout Finished');
10}, 1000);
11console.log('End of Script');
12
13
14// Output:
15// Started Script
16// End of Script
17// Timeout Finished
Diagram Loading...
A Promise in JavaScript is an object that represents the eventual completion (success) or failure of an asynchronous operation and its resulting value.
1const getPromiseValue = () =>
2 new Promise(resolve => {
3 console.log('Promise is being resolved...');
4 setTimeout(() => {
5 // ASYNC due to setTimeout, not the promise
6 resolve('Promise resolved successfully!');
7 }, 1000);
8 });
9
10console.log('This is a message before the promise is resolved.');
11
12getPromiseValue()
13 .then(value => {
14 // ASYNC
15 console.log(value); // Promise resolved successfully!
16 })
17 .catch(error => {
18 // ASYNC
19 console.error('Error:', error);
20 });
21
22// Runs immediately, does not wait for the promise to resolve
23console.log('This is a message after the promise is resolved.');
24
25// Output:
26//
27// Synchronous code:
28//
29// This is a message before the promise is resolved.
30// Promise is being resolved...
31// This is a message after the promise is resolved.
32//
33// Asynchronous code:
34// Promise resolved successfully!
35
.then
and .catch()
are methods that allow you to handle the result of the promise once it is resolved or rejected, respectively.
These methods run asynchronously, meaning they don't block the execution of the code that follows them.
new Promise((resolve, reject) => )
is a constructor that takes a callback function as an argument. This callback function is executed immediately, only the .then()
and .catch()
methods are executed later, when the promise is resolved or rejected.
Callback hell is a situation where you have multiple nested callbacks, resulting in code that is hard to read and maintain. This often happens when you have to perform multiple asynchronous operations in sequential order.
1const task2 = callback => {
2 setTimeout(() => {
3 console.log('Task 2 completed');
4 callback();
5 }, 2000);
6};
7
8const task1 = callback => {
9 setTimeout(() => {
10 console.log('Task 1 completed');
11 callback();
12 }, 1000);
13};
14
15task1(() => {
16 task2(() => {
17 console.log('All tasks completed');
18 });
19});
20console.log('Exiting...');
21
22
23// Output:
24// Exiting...
25// Task 1 completed
26// Task 2 completed
27// All tasks completed
1const task2 = () =>
2 new Promise((resolve, reject) => {
3 setTimeout(() => {
4 console.log('Task 2 completed');
5 resolve();
6 }, 2000);
7 });
8
9const task1 = () =>
10 new Promise((resolve, reject) => {
11 setTimeout(() => {
12 console.log('Task 1 completed');
13 resolve();
14 }, 1000);
15 });
16
17task1()
18 .then(() => task2())
19 .then(() => console.log('All tasks completed'))
20 .catch(error => console.error('An error occurred:', error));
21
22console.log('Exiting...');
23
24// Output:
25// Exiting...
26// Task 1 completed
27// Task 2 completed
28// All tasks completed
As you can see in the above example, we can chain multiple promises, but it can still get messy. Async/Await is a syntactic sugar over Promises that allows us to write asynchronous code in a more synchronous-looking manner. It makes the code easier to read and maintain.
1const task2 = () =>
2 new Promise((resolve, reject) => {
3 setTimeout(() => {
4 console.log('Task 2 completed');
5 resolve();
6 }, 2000);
7 });
8
9const task1 = () =>
10 new Promise((resolve, reject) => {
11 setTimeout(() => {
12 console.log('Task 1 completed');
13 resolve();
14 }, 1000);
15 });
16
17const runTasks = async () => {
18 console.log('Starting tasks...');
19 await task1();
20 await task2();
21 console.log('All tasks completed');
22}
23
24runTasks()
Only if the function is declared as async
, you can use the await
keyword. The await
keyword pauses the execution of the async
function until the promise is resolved or rejected.
1const getPromiseValue = async () =>
2 await new Promise(resolve => {
3 resolve('Promise resolved successfully!');
4 });
5
6const main = async () => {
7 console.log('Starting main function...');
8 const value = await getPromiseValue();
9 console.log(value);
10 console.log("After promise resolved");
11}
12
13main();
14console.log('Main function completed.');
15
16// Output:
17//
18// Synchronous code
19// Starting main function...
20// Main function completed.
21//
22// Asynchronous code
23// Promise resolved successfully!
24// After promise resolve
If you forget to add the async
keyword, you will get an Promise {<pending />}
response and the async
function will not pause its execution.
1const getPromiseValue = async () =>
2 await new Promise(resolve => {
3 resolve('Promise resolved successfully!');
4 });
5
6const main = async () => {
7 console.log('Starting main function...');
8 const value = getPromiseValue();
9 console.log('Promise value:', value);
10 console.log("After promise resolved");
11}
12
13main();
14console.log('Main function completed.');
15
16// Output:
17//
18// Starting main function...
19// Promise value: Promise { <pending> }
20// After promise resolved
21// Main function completed.
Diagram Loading...
The event loop is a mechanism that allows JavaScript to perform asynchronous operations. It continuously checks the call stack and the callback queues to see if there are any tasks that need to be executed. If the call stack is empty, it will take the first task from the callback queue and push it onto the call stack for execution.
The microtask queue is prioritized over the macrotask queue. This means that if there are tasks in both queues, the event loop will first execute all the tasks in the microtask queue before moving on to the macrotask queue. This is why promises and mutation observer callbacks are executed before any other tasks in the macrotask queue.
1// 1. process.nextTick (Node.js only)
2process.nextTick(() => console.log('process.nextTick microtask'));
3
4// 2. Promise callbacks
5Promise.resolve().then(() => console.log('Promise microtask'));
6
7// 3. queueMicrotask
8queueMicrotask(() => console.log('queueMicrotask microtask'));
9
10// 4. MutationObserver (browser only)
11const observer = new MutationObserver(
12 () => console.log('MutationObserver microtask'));
13observer.observe(document.body, { childList: true });
14document.body.appendChild(document.createElement('div'));
15// triggers observer
16
process.nextTick()
is a Node.js always has a higher precedence than the rest of the items in the microtask queue. It is executed before any other microtask, even if it was added later.
The remaining items in the microtask queue are executed in the order they were added.
1// Timer phase, runs first
2// 1. setTimeout
3setTimeout(() => console.log('setTimeout macrotask'), 0);
4
5// 2. setInterval
6const interval = setInterval(() => {
7 console.log('setInterval macrotask');
8 clearInterval(interval);
9}, 0);
10//
11
12// 3. UI events (browser only)
13document.body.addEventListener('click',
14 () => console.log('UI event macrotask'));
15
16// 4. MessageChannel
17const channel = new MessageChannel();
18channel.port1.onmessage = () => console.log('MessageChannel macrotask');
19channel.port2.postMessage('ping');
20
21// 5. setImmediate (Node.js only)
22setImmediate(() => console.log('setImmediate macrotask'));
23
24// 6. I/O events (Node.js, browser)
25const fs = require('fs');
26fs.readFile(__filename, () => console.log('I/O macrotask'));
In browsers, the concept of phases is less formalized. However in both browsers and Node.js, the timer
phase is executed first.
Understanding the event loop is crucial for mastering asynchronous programming in JavaScript. It allows you to write non-blocking code, making your applications more responsive and efficient.
I hope to see you in the next article.