JavaScript execution context is a crucial part of understanding how JavaScript works behind the scenes. So lets understand it first.
Execution Context:
When the JavaScript engine scans a script file, it makes an environment called the Execution Context that handles the entire transformation and execution of the code.
During the context runtime, the parser parses the source code and allocates memory for the variables and functions. The source code is generated and gets executed.
There are two types of execution contexts: global and function. The global execution context is created when a JavaScript script first starts to run, and it represents the global scope in JavaScript. A function execution context is created whenever a function is called, representing the function's local scope.
Now lets see how js executes code with an Example.
function funcA(m,n) {
return m * n;
}
function funcB(m,n) {
return funcA(m,n);
}
function getResult(num1, num2) {
return funcB(num1, num2)
}
var res = getResult(5,6);
console.log(res); // 30
In this example, the JS engine creates a global execution context that enters the creation phase.
First it allocates memory for funcA
, funcB
, the getResult
function, and the res
variable. Then it invokes getResult()
, which will be pushed on the call stack.
Then getResult()
will call funcB()
. At this point, funcB
's context will be stored on the top of the stack. Then it will start executing and call another function funcA()
. Similarly, funcA
's context will be pushed.
Once execution of each function is done, it will be removed from the call stack. The following picture depicts the entire process of the execution:
Understanding synchronous and asynchronous nature of javascript
Synchronous in JavaScript :
As its base JavaScript language is synchronous. Synchronous means the code runs in a particular sequence of instructions given in the program. Each instruction waits for the previous instruction to complete its execution.
See the following code example of synchronous JavaScript
console.log('First');
console.log('Second');
console.log('Third');
//output
//First
//Second
//Third
In the above JavaScript code snippet, three lines of instructions are given. Every instruction runs once after the previous instruction gets executed. Due to this synchronous nature of javascript, we get the output of console logs in the sequence we provided in the program.
Asynchronous in JavaScript :
As we saw in the synchronous code example, all instructions in the program execute one after another, and every instruction waits for its previous instruction to get executed. Due to this nature of synchronous programming, sometimes important instructions get blocked due to some previous instructions, which causes a delay in the user interface. Asynchronous code execution allows to execution next instructions immediately and doesn’t block the flow because of previous instructions.
See the following coding example to understand how javascript works asynchronously.
console.log('First');
// Set timeout for 2 seconds
setTimeout(() => console.log('Second'), 2000);
console.log('Third');
//First
//Third
//Second
As we can see in the output of the above code example, Third gets printed before Second, because of the asynchronous execution of the code. Here setTimeout() function waits for 2 seconds, and in the meantime, the next instruction gets executed without waiting for the previous one to complete the execution.
Conclusion
Javascript is the synchronous single-threaded language but with the help of event-loop, promises and callbacks, JavaScript is used to do asynchronous programming.
Understanding the power of async javascript with the help of callbacks, promises and async/await.
Lets start with callbacks:
callbacks
A callback is a function that is passed inside another function, and then called in that function to perform a task.
Confusing? Let's break it down by practically implementing it.
console.log('fired first');
console.log('fired second');
setTimeout(()=>{
console.log('fired third');
},2000);
console.log('fired last');
//output:
// fired first
// fired second
// fired last
// fired third
The snippet above is a small program that logs stuff to the console. But there is something new here. The interpreter will execute the first instruction, then the second, but it will skip over the third and execute the last.
The function inside setTimeout
in this case is required to run after two seconds (2000 milliseconds). Imagine it being carried off to be executed in some separate part of the browser, while the other instructions continue executing. After two seconds, the results of the function are then returned.
That is why if we run the above snippet in our program, we will get this:
fired first
fired second
fired last
fired third
You see that the last instruction is logged before the function in the setTimeout
returns its result. Say we used this method to fetch data from a database. While the user is waiting for the database call to return results, the flow in execution will not be interrupted.
This method was very efficient, but only to a certain point. Sometimes, developers have to make multiple calls to different sources in their code. In order to make these calls, callbacks are being nested until they become very hard to read or maintain. This is referred to as Callback Hell.
Lets look one example here:
const makeBurger = nextStep => {
getVegetables(function(vegetables) {
cookVegetables(vegetables, function(cookedvegetables) {
getBuns(function(buns) {
putVegetablesBetweenBuns(buns, vegetables, function(burger) {
nextStep(burger);
});
});
});
});
};
The next issue of callbacks has to do with inversion of control. when you write a callback, you're assuming that the program you're giving the callback to is responsible and will call it when (and only when) it's supposed to. You're essentially inverting the control of your program over to another program.However, for many third-party libraries, callback functions are the interface for how you interact with them. It's entirely plausible that a third party library could, whether on purpose or accidentally, break how they interact with your callback.
function criticalFunction() {
// It's critical that this function
// gets called and with the correct
// arguments.
}
thirdPartyLib(criticalFunction);
Since you're not the one calling criticalFunction
, you have 0 control over when and with what argument it's invoked. Most of the time this isn't an issue, but when it is, it's a big one.
To fix these problems, promises were introduced.
Lets understand Promises :
Promises
We hear people make promises all the time, but promises in JavaScript are slightly different.Its definition from mdn docs is:
The Promise
object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
There are three stages of a promise:
Pending:
pending
is the default, initial state.Fulfilled: when the promise successfully resolved.
Rejected: when some error occured.
Promises came along to solve the problems of callback functions. A promise takes in two functions as parameters. That is, resolve
and reject
. Remember that resolve is success, and reject is for when an error occurs.
const getData = (dataEndpoint) => {
return new Promise ((resolve, reject) => {
//some request to the endpoint;
if(request is successful){
//do something;
resolve();
}
else if(there is an error){
reject();
}
});
};
The code above is a promise, enclosed by a request to some endpoint. The promise takes in resolve
and reject
like I mentioned before.
Using ‘Then’ (Promise Chaining):
Promises are a neat way to fix problems brought about by callback hell, in a method known as promise chaining. You can use this method to sequentially get data from multiple endpoints, but with less code and easier methods.
Promise.resolve('some')
.then(function(string) { // <-- This will happen after the above Promise resolves (returning the value 'some')
return new Promise(function(resolve, reject) {
setTimeout(function() {
string += 'thing';
resolve(string);
}, 1);
});
})
.then(function(string) { // <-- This will happen after the above .then's new Promise resolves
console.log(string); // <-- Logs 'something' to the console
});
Promise Methods
Promise.all( ) :The
Promise.all()
method accepts an iterable Object, such as an Array of promises as an input and returns a single promise that resolves to a result array of the input promises.It rejects immediately when an input promise rejects or non-promise throws an error and will reject with that first rejection message / error message.const promise1 = new Promise((resolve, reject) => { setTimeout(resolve, 300, "resolved"); }); //will be resolved after 300ms const promise2 = 93; //non-promise const promise3 = new Promise((resolve, reject) => { setTimeout(resolve, 100, "resolved2"); }); // will be resolved after 100ms Promise.all([promise1, promise2, promise3]) .then((values) => { console.log(values); }) .catch((err) => { console.log(err); }); //expected output: [ 'resolved', 93, 'resolved2' ]
Promise.any( ) : The
Promise.any()
takes an iterable Object, such as an Array of promises as an input. Once a promise is fulfilled, a single promise is returned and the promise is resolved using the value of the promise.If no promises in the iterable fulfill (if all of the given promises are rejected), then the returned promise is rejected with an AggregateError (that groups together individual errors).UnlikePromise.all()
, this method is used to return the first promise that fulfills.const SlowlyDone = new Promise((resolve, reject) => { setTimeout(resolve, 500, "Done slowly"); }); //resolves after 500ms const QuicklyDone = new Promise((resolve, reject) => { setTimeout(resolve, 100, "Done quickly"); }); //resolves after 100ms const Rejection = new Promise((resolve, reject) => { setTimeout(reject, 100, "Rejected"); //always rejected }); Promise.any([SlowlyDone, QuicklyDone, Rejection]) .then((value) => { console.log(value); // QuicklyDone fulfils first }) .catch((err) => { console.log(err); }); //expected output: Done quickly
Promise.race( ) : The
Promise.race()
method returns a Promise that is resolved or rejected, as soon as one of the promises in an iterable, such as an array, fulfills or rejects, with the value or reason from that Promise.The promise returned will be forever pending, if the iterable passed is empty.const pro1 = new Promise((resolve, reject) => { setTimeout(() => resolve("one"), 200); }); const pro2 = new Promise((resolve, reject) => { setTimeout(() => resolve("two"), 100); }); Promise.race([pro1, pro2]) .then((response) => { console.log(response); //output: two }) .catch((err) => { console.log(err); }); const pro3 = new Promise((resolve, reject) => { setTimeout(() => reject("rejected"), 300); }); const pro4 = new Promise((resolve, reject) => { setTimeout(() => resolve("four"), 400); }); Promise.race([pro3, pro4]) .then((response) => { console.log(response); }) .catch((err) => { console.log(err); }); //output: rejected
In the first case, pro2(100ms) is faster than pro1(200ms), so the output shows two. In the second case, pro3(300ms) is faster than pro4(400ms), so the promise rejects. So basically, Promise.race() takes the first settled Promise.
Promise.allSettled( ):accepts an array of promises as an argument and returns a single promise as the output.
The single promise it returns will always resolve or enter the state ‘fulfilled’ after all the input promises are settled. It does not care if any individual promise in the input array rejected. The array
Promise.all()
resolves with will contain the resolve values or rejection reasons of promises in the input array.const promise1 = new Promise((resolve) => setTimeout(resolve, 3000, `First Promise's Value`) ); const promise2 = new Promise((resolve) => setTimeout(resolve, 2000, `Second Promise's Value`) ); const promise3 = Promise.reject(`Third Promise's Value`); Promise.allSettled([promise1, promise2, promise3]); // Output on the console // *Promise {<fulfilled>: Array(3)}* Promise.allSettled([promise1, promise2, promise3]).then(console.log); // Output on the console /* (3) [{…}, {…}, {…}] 0: {status: 'fulfilled', value: "First Promise's Value"} 1: {status: 'fulfilled', value: "Second Promise's Value"} 2: {status: 'rejected', reason: "Third Promise's Value"} */
From the example above, you can see that even though promise3
rejects on creation, Promise.allSettled()
still returned a ‘fulfilled’ promise. It does this even if all the promises in the input array reject.
How to Handle Errors in a Promise:
To handle errors in Promises, use the .catch()
method. If anything goes wrong with any of your promises, this method can catch the reason for that error.
Promise.reject(new Error())
.catch((reason) => console.error(reason));
// Error
This time in our example, the error output is no longer ‘uncaught’ because of .catch()
.
You can also use the .catch()
method in a chain of promises. It catches the first error it encounters in the chain.
But there is an even better way! You might be familiar with the following method, as it's a preferred way of handling data and API calls in JavaScript.
Async and Await in JavaScript
The thing is, chaining promises together just like callbacks can get pretty bulky and confusing. That's why Async and Await was brought about.
To define an async function, you do this:
const asyncFunc = async() => {
}
Note that calling an async function will always return a Promise.
Let's really break down some code now. Consider the little snippet below:
const asyncFunc = async () => {
const response = await fetch(resource);
const data = await response.json();
}
The async
keyword is what we use to define async functions as I mentioned above. But how about await
? Well, it stalls JavaScript from assigning fetch
to the response variable until the promise has been resolved. Once the promise has been resolved, the results from the fetch method can now be assigned to the response variable.
The same thing happens on line 3. The .json
method returns a promise, and we can use await
still to delay the assigning until the promise is resolved.
When I say 'stalling', you must think that implementing Async and Await somehow blocks code execution. Because what if our request takes too long, right?
Fact is, it doesn't. Code that is inside the async function is blocking, but that doesn't affect program execution in any way. The execution of our code is just as asynchronous as ever. To show this,
const asyncFunc = async () => {
const response = await fetch(resource);
const data = await response.json();
}
console.log(1);
cosole.log(2);
asyncFunc().then(data => console.log(data));
console.log(3);
console.log(4);
In our browser console, the output of the above would look something like this:
1
2
3
4
data returned by asyncFunc
You see that as we called asyncFunc
, our code continued running until it was time for the function to return results.
Understanding browser API, callback queue, microtask queue and event loop
Web Browser APIs
Web APIs are typically provided by the browser or the runtime itself, and are used to interact with various web-based resources, such as network requests, timers, user interfaces, and more.
Some common Web APIs provided by JavaScript runtime environments include:
DOM (Document Object Model) API
Fetch API
setTimeout
Web Storage API
The Callback Queue
In JavaScript, there are two types of task queues: the callback queue and the microtask queue. Both queues hold tasks that are executed asynchronously, but there are some differences in how they work.
The Callback Queue (Macrostack Queue), also known as the task queue, holds tasks that are pushed to the queue by Web APIs, such as setTimeout, setInterval, XMLHttpRequest, or events like mouse clicks and keyboard inputs. When the call stack is empty, the event loop moves the first task from the callback queue to the call stack for execution.
For example, consider the following code:
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
In this code, the console.log('1') statement is executed first, followed by the setTimeout function. The setTimeout function schedules the anonymous function passed to it to be executed after 0 milliseconds, which means that it will be added to the callback queue after the current call stack is empty. The console.log('3') statement is executed next.
When the current call stack is empty, the event loop moves the anonymous function from the callback queue to the call stack for execution. Therefore, the output of the above code will be:
1
3
2
Microtask Queue
Microtask Queue, also known as the Job queue or Promise queue, holds tasks that are pushed to the queue by microtasks, such as Promise.resolve, Promise.reject, or queueMicrotask. When the call stack is empty and there are no pending tasks in the callback queue, the event loop moves the first task from the microtask queue to the call stack for execution.
For example, consider the following code:
console.log('1);
Promise.resolve().then(() => console.log('2'));
console.log('3');
In this code, the console.log('1') statement is executed first, followed by the Promise.resolve() function. The .then method schedules the anonymous function passed to it to be executed after the current call stack is empty, which means that it will be added to the microtask queue. The console.log('3') statement is executed next.
When the current call stack is empty and there are no pending tasks in the callback queue, the event loop moves the anonymous function from the microtask queue to the call stack for execution. Therefore, the output of the above code will be:
1
3
2
So, the main difference between the Callback Queue and the Microtask Queue is the order in which they are processed by the event loop. The microtask queue has more priority than callback queue.
Event loop
An event loop is a looping algorithm or we can say a job scheduling algorithm that schedules the events based on the priorities of the task and then executes it.
This algorithm makes use of a queue data structure(microtask queue and callback queue) for scheduling the tasks and then it uses a stack data structure called Call stack for executing the tasks.
The event loop simply check the call stack and if its empty it takes the oldest callback and pushes it into callstack which eventually executed.