Media Books Khan Yousaf Advanced JavaScript Unleashed 2024
Media Books Khan Yousaf Advanced JavaScript Unleashed 2024
Confusing Parts
Yousaf Khan
* * * * *
This is a Leanpub book. Leanpub empowers authors and publishers with the
Lean Publishing process. Lean Publishing is the act of publishing an in-
progress ebook using lightweight tools and many iterations to get reader
feedback, pivot until you have the right book and build traction once you
do.
* * * * *
This course aims to teach different concepts in JavaScript that are not easy
to grasp, especially for beginners. Topics like closures, coercion, the
asynchronous nature of JavaScript, etc., are examples of topics that most
beginners struggle with because they are not easy to understand. The goal
of this course is to provide in-depth, easy-to-understand explanations of
such confusing topics.
Even those who have been working with JavaScript for a few years might
need help understanding some of the concepts covered in this course or
might have some gaps in their understanding. The goal of this course is to
fill those gaps.
By the end of this course, students will have a deep understanding of the
concepts covered in this course. They will become better at JavaScript by
having a solid understanding of the topics that most JavaScript beginners
struggle with. Students will be able to debug JavaScript code better and
avoid common pitfalls by having a deep understanding of fundamental but
confusing JavaScript topics.
Prerequisites
This course assumes that students have a basic understanding of JavaScript
and programming in general. Understanding of topics like variables, loops,
objects, arrays, functions, etc., is assumed.
You can download code examples from the same place where you
purchased this book.
If you have any trouble finding or downloading the code examples, email us
at [email protected].
So, how did this programming language that we know today come into
existence? Let’s take a brief tour of its history.
History of JavaScript
In the early days of web browsers, web pages could only be static, without
any dynamic behavior or interactivity. A server was needed for simple
things like form input validations. With limited internet speeds in those
days, requiring a roundtrip to the server just for input validation was highly
undesirable.
There was a need for a client-side scripting language that allowed us to add
interactivity to web pages. In 1995, a software developer at Netscape began
working on such a language, initially called “mocha,” which was later
changed to “LiveScript.”
As mentioned in the previous lesson, the initial name for JavaScript was
Mocha, which was later changed to LiveScript, and just before the release
of the Netscape Navigator 2 browser, the name was changed to JavaScript.
So what’s the reason behind using the word “Java” inside the name
“JavaScript” when there was already a popular programming language
named “Java”?
This question of whether these two languages are related to each other
arises in the mind of almost every person who is already familiar with one
of these two languages and comes across the other language.
It was just a marketing move. Java was popular at that time, and Netscape
wanted to cash in on the popularity of the Java language. That is why
Netscape renamed the language from LiveScript to JavaScript.
Apart from some similarities in the syntax and, of course, in the name of
both languages, the two languages are very different from each other.
Other people can become part of TC39 as well and make their contribution
in the development of the JavaScript language. Details of how anyone can
either contribute or be part of TC39 meetings as a member can be found on
the TC39 website.
TC39 also has a GitHub account which hosts multiple repositories related to
their work. Repositories include notes from TC39 meetings, different
proposals for the JavaScript language, details of how TC39 works, etc.
Proposals process
TC39 has a well-defined process through which proposals are passed before
they can be added to the ECMAScript specification and ultimately be part
of the JavaScript language.
Each proposal is passed through five stages which are mentioned below:
process stages
Stage 0
The first stage represents someone’s idea that they think is worthy of being
part of the JavaScript language.
Not all proposals at this stage move to the next stage, but the ones that do
are the ones that gain enough interest and have a TC39 committee member
who is willing to work on the idea. Proposals at this stage that gain enough
interest are discussed in the TC39 meeting. If the discussion goes well, the
proposal is moved to the next stage.
Stage 1
Stage 2
At this stage, the API, syntax, and so on are refined and described in greater
detail using the formal specification language. Polyfills and Babel plugins
may be developed at this stage for real-world experimentation with the
solution of the proposal.
Once the proposal has been described in enough detail, it can be considered
for the next stage.
Stage 3
For proposals to be moved to the final stage, they need to meet the
following two criteria:
a suite of tests
two compatible implementations of the proposal that passed the tests
Once the conditions mentioned above are met, and the committee has a
consensus that the proposal is ready to be part of the ECMAScript
specification, it is moved to the next stage.
Stage 4
Keeping ourselves updated with new changes is one thing, but how can we
track changes that could potentially be part of JavaScript in the future?
Notes and proposal repositories are a great place to keep track of upcoming
changes, and in general, the TC39 GitHub repository is useful for tracking
the work that the TC39 committee is doing.
As the 6th edition came out in the year 2015, it was “ECMAScript 2015”,
“ES2015” for short. The later editions were also named using the year
number in which they came out, e.g., “ES2016”, etc.
You will also see these editions referred to as “ES6”, “ES7”, etc., and this
naming convention is also fine to use. Which one you choose is up to your
preference. Most of the time, you will see both types of names used
interchangeably.
JavaScript code is converted into byte code, and the JavaScript engine then
executes this byte code. However, modern JavaScript engines perform
many optimizations to increase the performance of JavaScript code. These
optimizations are performed based on the information collected by the
engine while it is executing the code.
These “hot” parts of the code are then compiled into native machine code,
and this machine code is then executed instead of the corresponding byte
code.
While Javascript code is still distributed in source code format rather than
executable format, it is compiled into byte code and possibly native
machine code.
JavaScript Engine
The following table shows some major browsers and their JavaScript
engines.
Browser Engine
Google Chrome V8
Edge Chakra
Mozilla Firefox Spider Monkey
Safari JavaScriptCore
While there are differences in the steps taken by each JavaScript engine to
execute the JavaScript code, the major steps taken by each engine are more
or less the same and we will try to have a high-level overview of how our
code gets transformed and executed by the JavaScript engines by
understanding the Google Chrome’s V8 engine.
:::note Please note that the team working on the V8 engine is continuously
improving it; as a result, the simplified execution pipeline shown in the
image above may change in the future. :::
Let’s get a better understanding of how the execution pipeline shown above
works by understanding what happens at each step of this pipeline.
Source Code
Before the JavaScript engine can begin its work, the source code needs to
be downloaded from some source. This can either be from the network, a
cache, or a service worker that pre-fetched the code.
The engine itself doesn’t have the capability to download the code. The
browser does it and then passes it to the engine, which can then begin
transforming it and eventually execute it.
Parser
After downloading the source code, the next step is to transform it into
tokens. Think of this step as identifying different parts of the code; for
example, the word “function” is one token that is identified as a “keyword.”
Other tokens may include a string, an operator, etc. This process of dividing
the code into tokens is done by a “scanner,” and this process is known as
“tokenization.”
Once the tokens have been generated, the parser uses them to generate an
Abstract Syntax Tree (AST), a set of objects that represent the structure of
the source code.
AST Explorer is a cool website that you can use to visualize the AST. Go
ahead and paste the code above in the AST explorer and explore the
generated AST.
Interpreter
The Bytecode Generator uses the AST produced by the parser to generate
the bytecode. This generated bytecode is taken by the Bytecode
Interpreter, which then interprets it.
Compiler
For example, the JavaScript engine can identify the parts of code that are
being executed frequently, also known as the “hot” parts of the code. The
“hot” parts of the code are then compiled into native machine code to
ensure that these parts get executed as fast as possible.
The need for falling back to the bytecode arises from the fact that
JavaScript is a dynamically typed language. The dynamic nature means that
we can call a particular JavaScript function with different kinds of values.
and if it is compiled to native machine code, but then if the same function is
called with an object with a different shape, the JavaScript engine cannot
use the optimized machine code and has to fall back to the bytecode.
References:
The following resources can be used to learn more about how the
JavaScript code is executed:
:::
The following are two main types of execution contexts that we will
discuss:
:::info
There is a third type of execution context that is created for the execution of
code inside the eval function. Still, as the use of the eval function is
discouraged due to security concerns, we will only discuss the types of
execution context mentioned above.
:::
The global context contains the global variables, functions, etc. It also
contains the value for this and a reference to the outer environment, which,
in the case of a global execution context, is null.
The function execution context also contains the arguments passed to the
function.
Creation phase
Execution phase
Creation phase
As the name suggests, the execution contexts (global and function) are
created during the creation phase.
During this phase, the variable declarations and references to functions are
saved as key-value pairs inside the execution context. The value of this
and a reference to the outer environment are also set during this phase.
The values for variables are not assigned during the creation phase.
However, variables that refer to functions do refer to functions during this
phase. Variables declared using var are assigned undefined as their value
during this phase, while variables declared using let or constants declared
using const are left uninitialized.
:::info
:::
During the creation phase, the following two components are created:
Lexical environment
Variable environment
Lexical and Variable environments are structures that are used internally to
hold key-value mappings of variables, functions, reference to the outer
environment, and the value of this.
The difference between the lexical and variable environments is that the
variable environment only holds the key-value mappings of variables
declared with the var keyword, while function declarations and variables
declared with let or constants declared using const are inside the lexical
environment.
The execution context for the above code during the creation phase can be
conceptually visualized as shown in the image below:
global execution context
Execution phase
References
Following resources can be used to learn more about the execution context
in JavaScript:
:::note
:::
Note that in the above image, before the execution of the console.log
statement, there is something named “global” in the call stack. This is a
global execution context that is created and pushed on the call stack before
executing the code.
:::note
The label “global” is not important, and different browsers may use
different labels to represent the global execution context in the call stack.
For example, the debugger in the Google Chrome browser shows
“(anonymous)” instead of “global.” The above screenshot was taken using
the Firefox browser.
:::
After pushing the global execution context on the call stack, any function
calls encountered during the execution of the code will lead to more entries
in the call stack. For every function call, a new entry is added to the call
stack before that function starts executing, and as soon as the function
execution ends, that entry is popped off the stack. Consider the following
code:
Before the execution of the above code, the global execution context is
pushed on the call stack.
callstack
As soon as the foo function is called, a new entry is added to the call stack
for the execution of the foo function.
callstack
The foo function contains a call to the baz function. So, another entry is
pushed on the call stack for the baz function call.
callstack
The baz function contains a call to the bar function. So, another entry is
pushed on the call stack for the bar function call.
callstack
Note that the top element in the call stack represents the currently executing
piece of code. As soon as the bar function execution ends, its entry in the
call stack is removed. Ultimately, the code execution completes, and the call
stack becomes empty.
You can run the code above in the Replit below to see the call stack in
action:
<ReplitEmbed src=”https://replit.com/@newlineauthors/Lesson-9-
callstack” />
Stack overflow
The term “stack overflow” is a familiar term for every software developer,
either because of every software developer’s favorite website stackoverflow
or because of the stack overflow error due to infinite recursion.
We will be discussing the term “stack overflow” in the context of the call
stack. The call stack has a fixed size and can contain a limited number of
entries.
There are many resources on the internet that claim that primitives are
allocated on the stack and objects are allocated on the heap, but the reality
is not that simple.
The official ECMAScript specification doesn’t state anything about how
JavaScript engines should allocate memory for different types of values or
how they should free up memory once it is no longer needed by the
program. As a result, different JavaScript implementations are free to
choose how they want to handle memory management in JavaScript.
So instead of simply believing that the primitive values are stored on the
stack, and the objects are stored on the heap, we should understand that
memory allocation in JavaScript is an implementation detail, and different
JavaScript engines might handle memory differently because the language
specification doesn’t mandate how memory should be handled in
JavaScript.
In the V8 engine, for example, almost everything is stored on the heap. The
following quote from the official V8 blog invalidates the common
misconception regarding memory allocation in JavaScript.
This doesn’t mean that we should assume that everything is allocated on the
heap. JavaScript engines may allocate most values on the heap but could
use the stack for optimization and store temporary values that might not last
longer than a function call.
The most important point you should take away from this lesson is that
different JavaScript engines may handle memory differently, and
“primitives in javaScript simply go on the stack” is a misconception.
Further reading
The Java language also has the mechanism of automatic garbage collection,
but in Java, programmers can manually trigger the garbage collection
process, whereas JavaScript programmers don’t have this level of control
over garbage collection. Some might see this as a limitation, but there is no
doubt that automatic garbage collection is really helpful for programmers to
avoid memory leaks that are often encountered in languages that don’t
handle this automatically.
Hoisting
In JavaScript, variables and functions can be accessed before their actual
declaration, and the term used to describe this in JavaScript is “Hoisting.”
“var” declarations
<ReplitEmbed src=”https://replit.com/@newlineauthors/hoisting-
example1” />
The output of the above code shows hoisting in action. The first line in the
above code outputs undefined on the console, but how is this possible?
How are we able to access the result variable before it is actually declared
on the second line?
This is made possible because of the parsing step before the code is
executed. The preprocessing of the JavaScript code before its execution
allows the JavaScript engine to detect some errors early before any code is
executed. The following code example shows this in action:
1 function print(obj) {
2 console.log(obj; // error
3 }
4
5 console.log("hello world");
Without any preprocessing of the code, the JavaScript engine cannot detect
the syntax error inside the print function unless the function is invoked or
called, and the above code should log “hello world” on the console without
throwing any errors because the function containing the syntax error hasn’t
been called. But the above code throws an error instead of logging “hello
world” on the console. Why is that? The answer is the processing step
before the code execution.
The JavaScript engine scans the code before executing it, allowing it to
detect some errors before any code is executed. This also enables the engine
to handle variable declarations by registering the variables declared in the
current scope. Before any scope starts, all the variables declared in that
scope are registered for that scope. In other words, all the variables declared
in a scope are reserved for that scope before the code in that scope is
executed. This preprocessing of the code before its execution is what
enables hoisting. This allows us to refer to “var” variables before they are
actually declared in the code.
Let us revisit the code example given above that logs undefined to the
console.
1 console.log(result); // undefined
2 var result = 5 + 10;
If the “var” variables are hoisted, and we can refer to them before their
declaration, then why is undefined logged on the console instead of the
actual value of the result variable, which should be 15?
The thing with the hoisting of the “var” variables is that only their
declaration is hoisted, not their values. These variables are assigned the
value of undefined, and the actual value, 15 in the above code example, is
assigned when their declaration is executed during the step-by-step
execution of the code.
Function declarations
Function declarations, just like variables declared using the “var” keyword,
are also hoisted. The following code example shows the hoisting of a
function declaration in action:
1 startCar(); // starting the car...
2
3 function startCar() {
4 console.log("starting the car...");
5 }
<ReplitEmbed src=”https://replit.com/@newlineauthors/hoisting-
example4” />
Standard rules
According to the standard rules, the function declarations inside blocks are
hoisted to the top of the block, converted into a function expression, and
assigned to a variable declared with the let keyword.
The function hoisted inside the block is limited to the containing block and
cannot be accessed by code outside the block containing the function.
:::note
It is important to note that the standard rules only come into effect in strict
mode.
:::
The following code example will help you better understand the standard
rules for function declarations inside blocks:
1 "use strict";
2
3 function process() {
4 if (true) {
5 console.log("processing...");
6
7 function fnInsideBlock() {
8 console.log("I am inside a block");
9 }
10 }
11 }
According to the standard rules, the function inside the if block should be
treated as shown below:
1 "use strict";
2
3 function process() {
4 if (true) {
5 let fnInsideBlock = function fnInsideBlock() {
6 console.log("I am inside a block");
7 };
8
9 console.log("processing...");
10 }
11 }
The fnInsideBlock function in the above code can only be called from
within the if block.
Legacy rules
The legacy rules are applied to the non-strict code in web browsers.
According to the legacy rules, apart from a let variable for a function
declaration inside a block, there is also a var variable in the containing
function scope.
Let us take the code example given in the above section and add the
function calls:
1 "use strict";
2
3 function process() {
4 if (true) {
5 console.log("processing...");
6
7 function fnInsideBlock() {
8 console.log("I am inside a block");
9 }
10 }
11
12 fnInsideBlock();
13 }
14
15 process(); // error
<ReplitEmbed src=”https://replit.com/@newlineauthors/hoisting-
example7” />
As the above code is executed in strict mode, the standard rules apply; as a
result, the above code throws an error because, as explained in the above
section, the function inside the block is hoisted to the top of the containing
scope and is only accessible inside that block. As a result, the function call
outside the if block throws an error.
If the "use strict" directive is removed from the above code, it will
execute without any error. Why is that? This is because, in non-strict mode,
the legacy rules for function declarations in a block apply, and according to
the legacy rules, the hoisted function inside the block is assigned to a var
variable that is declared in the containing function scope.
<ReplitEmbed src=”https://replit.com/@newlineauthors/hoisting-
example8” />
:::note
Now, it should be clear why removing the “use strict” directive allows the
code to be executed without any error. The hoisted function inside the block
is assigned to a var variable defined in the function scope. As a result, the
function inside the block is accessible outside the containing if block.
:::tip
:::
Further reading
Class declarations
Like function declarations, class declarations are also hoisted, but they are
hoisted differently compared to the function declarations.
Let us first verify that class declarations are indeed hoisted with the help of
the following code example:
1 let Car = "Honda";
2
3 if (true) {
4 console.log(typeof Car); // error
5
6 class Car {}
7 }
<ReplitEmbed src=”https://replit.com/@newlineauthors/hoisting-
example9” />
The above code throws an error that proves that the class declarations are
indeed hoisted.
If the class declarations weren’t hoisted, then the console.log function call
should have logged “Honda” to the console, but that isn’t the case, and that
is because the class declaration inside the if block is hoisted and any code,
before or after the Car declaration inside the block, that accesses Car will
access the class declaration and not the Car variable declared above the if
statement. The fact that the identifier Car inside the if block refers to the
class declaration and not the Car variable declared before the if statement
proves that the class declarations are indeed hoisted.
So, if class declarations are hoisted, then why can’t we access them before
their declaration? The answer to this question is the “Temporal Dead Zone
(TDZ)”.
Temporal Dead Zone (TDZ) refers to the time during which the block-
scoped variables (let, const) or class declarations cannot be accessed. The
time starts from the start of the scope till the declaration is executed. The
following code example will help us visualize TDZ:
1 {
2 // TDZ start
3 console.log("hello");
4 console.log("world");
5 let count = 5; // TDZ end
6
7 // can access "count" after TDZ ends
8 }
TDZ is the reason class declarations cannot be accessed before their
declaration is executed during the step-by-step execution of the code.
As TDZ also applies to the let and const, are the variables declared using
let or constants using const also hoisted? Yes, they are also hoisted, but,
like the class declarations, they are hoisted differently because of the TDZ.
:::info
:::
1 var count = 5;
2
3 {
4 console.log(count); // hoisted but cannot access due to TDZ
5 let count = 10;
6 }
You can see the above code example in action in the following Replit:
<ReplitEmbed src=”https://replit.com/@newlineauthors/hoisting-
example11” />
The function and class expression are not hoisted. Consider the following
code example:
1 console.log(typeof Car); // undefined
2 console.log(typeof Person); // undefined
3
4 var Car = class {
5 constructor(model) {
6 this.model = model;
7 }
8 };
9
10 var Person = function (name) {
11 this.name = name;
12 };
<ReplitEmbed src=”https://replit.com/@newlineauthors/hoisting-
example12” />
In the above code example, as the variables Car and Person have been
declared using the var keyword, we can access them before their
declaration, and their value are undefined. This is because only the
declarations are hoisted, not their values. In the above code example, the
values are expressions, and they are not hoisted.
JavaScript engines don’t move the hoisted declarations to the top of the file.
Instead, they simply process the declarations before the step-by-step
execution of the code. In the case of var variables, they are assigned the
value of undefined until their declaration is executed. In the case of block-
scoped variables, they are marked as “uninitialized”.
Scope
The scope is a general concept in the field of computer science that refers to
the parts of the program where a particular variable, function, etc., can be
accessed. In other words, the scope of an identifier (variable, function, etc.)
is the part of a program where it is visible or can be referenced.
Modern JavaScript has four main types of scopes that are mentioned below:
Global scope
Function scope
Block scope
Module scope
Lexical scope
Scopes can be nested within other scopes, with each nested scope having
access to the outer or parent scope.
1 const myName = "John doe";
2
3 function hello() {
4 const greeting = "hello " + myName;
5 console.log(greeting);
6 }
The scope of the above declarations depends on where they are written in
the code structure above. The myName variable and hello function are both
in global scope, so they are available globally in the above code. The
greeting variable declaration is inside the hello function, so its scope is
local to the hello function.
:::info
:::
The global scope is generally the outermost scope that contains all other
scopes nested inside it. Each nested scope has access to the global scope. In
JavaScript, the global scope is the browser window or, more accurately, a
browser window tab. The global scope is exposed to the JavaScript code
using the window object.
The global scope is the parent scope of all other scopes; as a result, the
declarations inside the global scope are visible to all other scopes. This can
cause problems like variable name clashes, shadowing, etc. Another thing
about the global scope is that it isn’t destroyed until the application is
closed, so if we are not careful, declarations in the global scope can remain
in memory regardless of whether they are needed or not until the
application is closed.
Having said all that, declarations in the global scope are typically
unavoidable. So, the best we can do is avoid the global scope as much as
possible. Keep the global declarations to a minimum. If a variable is only
used inside a function, there is no point in declaring it in the global scope.
So, if you hadn’t heard it before, you are hearing it now: “Avoid polluting
the global scope.”
Implicit globals
<ReplitEmbed src=”https://replit.com/@newlineauthors/global-scope-
example3” />
Notice that the result variable is not declared. It is assigned the result of
multiplication as though it has already been declared. One would normally
expect an error in this case, but JavaScript will declare the result as a
global variable for you.
This is also one of the reasons always to write JavaScript code in strict
mode. Using the strict mode keeps such confusing behaviors of JavaScript
away from our code.
HTML attributes
Apart from the assignment to undeclared variables, there is another way we
get implicit global variables. The value of the id attribute or the name
attribute of HTML elements also gets added as a variable in the global
scope of JavaScript.
1 <h1 id="mainHeading">Hello World!</h1>
The id of the h1 element above gets added to the global scope as a variable.
We can access the h1 element using the mainHeading as a variable in
JavaScript code. This feature is referred to as Named access on the Window
object.
This was first added by the Internet Explorer browser and was gradually
implemented in other browsers as well, simply for compatibility reasons.
There are sites out there that rely on this behavior, so for the sake of
backward compatibility, browsers have no choice but to implement this
behavior.
Writing code that relies on this feature is a bad idea because it can result in
code that is hard to read and maintain. Imagine seeing an identifier in the
code and not being able to identify where it is declared. Such code is
vulnerable to name clashes with other variables in our code. Also, keep in
mind that writing code that depends on the HTML markup can break easily
if the HTML markup is changed. In short, avoid relying on this feature and
use better alternatives for targeting HTML elements in your JavaScript
code.
Further reading
Shadowing declarations
Declarations inside a nested scope can “shadow” the declarations with the
same name in the outer scope. This is referred to as “shadowing
declaration” or simply “shadowing.”
<ReplitEmbed src=”https://replit.com/@newlineauthors/function-scope-
example1”>
The variable hobby inside the function is shadowing the hobby variable
declared in the global scope.
<ReplitEmbed src=”https://replit.com/@newlineauthors/function-scope-
example2”>
In the code example above, we have shadowed the prefix variable declared
in the global scope, and the code inside the log function is unable to access
the global prefix variable.
:::note
Recall that the var declarations in the global scope are added as properties
on the window object. So, if the global prefix variable was declared using
the var keyword, we could have used window.prefix to access the
shadowed variable.
:::
If the function with non-simple parameters is defined in the global scope, its
parameter scope can be conceptually visualized as shown in the image
below:
The following code example proves that the non-simple parameters indeed
exist in a different scope than the function’s local scope:
1 function paramScope(arr = ["initial array"], buff = () => arr) {
2 var arr = [1, 2, 3];
3 console.log(arr); // [1, 2, 3]
4 console.log(buff()); // ["initial array"]
5 }
6
7 paramScope();
<ReplitEmbed src=”https://replit.com/@newlineauthors/function-scope-
example4”>
Inside the function, there is a var declaration with the same name as the
first parameter of the paramScope function. The two console.log calls
inside the function log two different values to the console; why is that?
Why does the buff parameter return the default value of the arr parameter
and not the value of the arr inside the local scope of the function?
The answer is that the arr parameter and the arr variable inside the
function are two different variables that exist in two different scopes. The
arr inside the function shadows the arr parameter, but calling the buff
function returns the parameter arr. If the parameter and the local arr were
the same variable, the buff function would return [1, 2, 3] instead of the
default value of the arr parameter.
Remove the var keyword inside the function to show the different output:
1 function paramScope(arr = ["initial array"], buff = () => arr) {
2 arr = [1, 2, 3];
3 console.log(arr); // [1, 2, 3]
4 console.log(buff()); // [1, 2, 3]
5 }
6
7 paramScope();
<ReplitEmbed src=”https://replit.com/@newlineauthors/function-scope-
example5”>
The arr inside the function now refers to the arr parameter, so any
assignment to arr inside the function is reflected when the buff function is
called.
In the code example above, the name of the function expression namedFn is
only accessible inside the function body. As a result, some might incorrectly
believe that the name of a named function expression is declared inside the
function body, but that is not correct; the name is declared in a different
scope. The following code proves this claim:
1 let fn = function namedFn() {
2 let namedFn = 123;
3 console.log(namedFn);
4 };
<ReplitEmbed src=”https://replit.com/@newlineauthors/function-scope-
example8”>
The let doesn’t allow the re-declaration of a variable. So if the nameFn was
declared inside the function scope, then the code example above should
have thrown an error; instead, there is no error, and this is valid code. The
nameFn inside the function body is actually shadowing the name of the
function expression.
Further reading
Block Scope
A block scope in JavaScript refers to the scope that exists between blocks of
code, such as if blocks or loops.
The block-scoped let and const solve problems like unnecessary exposure
of variables outside blocks, closure inside loops, etc. We will discuss the
closure inside loops problem in a module related to the topic of closure.
Another thing to know about the block scope is how function declarations
inside blocks are handled. This was discussed in a module related to
hoisting.
Module Scope
Modules are also among the features that were added in recent years to
JavaScript. Modules solve many problems related to code management that
existed in code bases that involved multiple JavaScript files. Modules allow
us to split our code into manageable chunks.
The code inside an ES module exists in the module scope. In other words,
the declarations inside a module are scoped to the module and aren’t
exposed outside of the module, except the code that is explicitly exported
from the module. Declarations at the top level of a module are limited to the
module and aren’t part of the global scope.
Further reading
Scope Chain
Every time a new scope is created, it is linked to its enclosing scope. This
linkage between different scopes creates a chain of scopes that can then be
used for lookups for identifiers that cannot be found in the current scope.
<ReplitEmbed src=”https://replit.com/@newlineauthors/scope-chain-
example1”>
The scope chain created as a result of the hello function call can be
conceptually visualized as shown below:
scope chain
The scope chain enables the lookup of identifiers that are not declared in the
current scope.
Optimizing scope chain lookup
Modern JavaScript apps contain many lines of JavaScript code that must be
compiled and executed in the shortest amount of time possible to ensure
that app users do not experience poor app performance. As a result, modern
JavaScript engines heavily optimize the JavaScript code we write to allow it
to execute as efficiently as possible. Different optimizations are performed
on the code while it is being transformed before it is executed.
Optimizations are not only limited to the steps before the code execution
but code is also optimized during its execution (recall the JIT compilation).
Further reading
So, if the developer expresses the intention to convert one type of value into
another, it is just type conversion. The following code example is an
example of an explicit type conversion:
1 const age = Number(ageStr);
To deep dive into the world of coercion, let us understand the following:
Abstract operations
Abstract equality operator (==)
Addition operation (+)
Relational operators (<, >, <=, >=)
ToPrimitive
ToNumber
ToString
ToBoolean
ToPrimitive
The ToPrimitive abstract operation is used to convert an object to a
primitive value. This operation takes two arguments:
OrdinaryToPrimitive
Each object in JavaScript inherits the following two methods from the
object that sits at the top of the inheritance hierarchy, i.e., the
Object.prototype object:
toString()
valueOf()
toString( )
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example1” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example2” />
valueOf( )
The valueOf method is used to convert an object into a primitive value. The
default implementation of this method, like the toString method, is
useless, as it just returns the object on which this method is called.
1 const arr = [];
2 arr.valueOf() === arr; // true
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example3” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example4” />
Prefer string
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example5” />
But it was mentioned above that the value returned by the toString method
can be of non-string primitive type. The following code example verifies
this claim:
1 const obj = {
2 toString() {
3 console.log("toString invoked");
4 return true;
5 },
6 valueOf() {
7 console.log("valueOf invoked");
8 return 123;
9 }
10 };
11
12 console.log(`${obj}`);
13 // toString invoked
14 // true
The toString method in the above code example returns a boolean (non-
string) primitive value. Instead of invoking the valueOf method or
converting the non-string return value of the toString method into a string
value, the boolean value is accepted as the primitive representation of the
obj object.
The next case we need to verify is what happens if the toString method
doesn’t return a primitive value. The following code example demonstrates
this case:
1 const obj = {
2 toString() {
3 console.log("toString invoked");
4 return [];
5 },
6 valueOf() {
7 console.log("valueOf invoked");
8 return 123;
9 }
10 };
11
12 console.log(`${obj}`);
13 // toString invoked
14 // valueOf invoked
15 // 123
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example7” />
The valueOf method is invoked even if the toString is not defined for an
object. The following code example shows this behavior:
1 const obj = {
2 toString: undefined,
3 valueOf() {
4 console.log("valueOf invoked");
5 return 123;
6 }
7 };
8
9 console.log(`${obj}`);
10 // valueOf invoked
11 // 123
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example8” />
The last case we need to verify is what happens when JavaScript can’t get a
primitive value, even after invoking the toString and the valueOf method.
1 const obj = {
2 toString() {
3 console.log("toString invoked");
4 return [];
5 },
6 valueOf() {
7 console.log("valueOf invoked");
8 return [];
9 }
10 };
11
12 console.log(`${obj}`);
13 // toString invoked
14 // valueOf invoked
15 // TypeError ...
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example9” />
When JavaScript can’t get a primitive value after invoking both methods, a
TypeError is thrown, indicating that the object couldn’t be converted into a
primitive value. So, it is important to remember when overriding these
methods that at least one of them should return a primitive value.
We have discussed what happens when the preferred type is a string in the
object-to-primitive conversion process. Next, let’s discuss what happens
when the preferred type is a number.
Prefer number
This is similar to the “prefer string” case discussed above, except that the
order in which the valueOf and the toString methods are invoked is the
opposite.
1 const obj = {
2 toString() {
3 console.log("toString invoked");
4 return "hello";
5 },
6 valueOf() {
7 console.log("valueOf invoked");
8 return 123;
9 }
10 };
11
12 console.log(obj + 1);
13 // valueOf invoked
14 // 124
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example10” />
This code example above is the same as the one shown in the section above,
except that instead of embedding obj in a template literal and logging it to
the console, we are adding 1 to obj. We are using obj as if it were a
number.
The rest of the cases are the same as discussed in the “prefer string” section.
The only difference is that the valueOf method is invoked first when the
preferred type is “number”.
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example11” />
Why did we get two as an output? The answer is that true is accepted as a
primitive representation of the obj object, but we cannot add true and 1.
JavaScript expects a number in this context. So, it tries to coerce true into
the expected type of value, which in this case is 1. If the valueOf method
had returned false, it would have been coerced to 0.
No preference
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example12” />
Out of the built-in objects, only the Date and Symbol objects override the
default behavior of the ToPrimitive abstract operation. The Date objects
implement the default behavior as if the preferred type or hint is “string.”
1 new Date()[Symbol.toPrimitive]("default"); // Tue Feb 07 2023 23:47:42 GMT+0500
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example13” />
:::note
ToNumber
The following table shows the results of this abstract operation applied to
some non-number values:
Value ToNumber(value)
”” 0
“0” 0
“-0” -0
” 123 “ 123
“45” 45
“abc” NaN
false 0
true 1
undefined NaN
null 0
The BigInt values allow explicit conversion into a number type, but the
implicit conversion is not allowed; implicit conversion throws a TypeError.
1 console.log(Number(10n)); // 10
2
3 console.log(+10n); // TypeError...
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-
operations-example14” />
ToString
The ToString abstract operation is used to convert any value into a string.
The following table shows the results of this abstract operation applied to
some non-string values:
Value ToNumber(value)
null “null”
undefined “undefined”
0 “0”
-0 “0”
true “true”
false “false”
123 “123”
NaN “NaN”
In the case of objects, the ToString abstract operation first converts the
object into a primitive value using the ToPrimitive abstract operation with
“string” as the preferred type, and then the resulting value is converted into
a string.
ToBoolean
false
0, -0, 0n
undefined
null
NaN
””
Let’s discuss the infamous “double equal” operator that is used for loosely
comparing two values. It is also known as the “abstract equality” operator.
Despite what you hear about his operator, it behaves according to some
predefined algorithmic steps, and if we understand how it works, this
operator won’t scare us, and we might even prefer this operator in some
cases over its cousin, the strict equality operator (===).
When two values are compared using the double equals operator, the steps
taken by JavaScript to compare the two values are described by an abstract
operation known as IsLooselyEqual.
If the values being compared are of the same type, then perform the
strict equality comparison.
If one value is undefined or null and the other value is also
undefined or null, return true.
If one or both values are objects, they are converted into primitive
types, preferring the number type.
If both values are primitives but are of different types, convert the
types until they match, preferring the number type for coercing values.
Think about the first point mentioned above. If the types of values being
compared using this operator are the same, under the hood, the two values
get compared using the triple equals or the strict equality operator. So, if we
know that only the same type of values will get compared in some piece of
code, it doesn’t make any difference whether we use the double equals or
the triple equals operator; in this case, we will always have the strict
equality operator.
null vs undefined
The second point is also worth pondering over. Unlike the strict equality
operator, the abstract or loose equality operator considers null ==
undefined comparison to be true.
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-equality-
operator-example1” />
The fact that null and undefined are equal to each other according to the
abstract equality operator makes for an interesting use case of the abstract
equality operator. When writing JavaScript code, it is common to check if a
value is neither null nor undefined.
With the strict equality operator, we will have a check that looks something
like the following:
1 if (someVal !== null && someVal !== undefined) {
2 // code
3 }
Whereas with the abstract equality operator, we can shorten this check to
just one condition:
1 if (someVal != null) {
2 // code
3 }
4
5 // or
6
7 if (someVal != undefined) {
8 // code
9 }
Considering how often we need to guard against null and undefined in our
JavaScript code, I feel the abstract equality operator is ideal for this case.
Having said that, you won’t be wrong if you use the strict equality operator
in this case.
“if” conditions
<ReplitEmbed src=”https://replit.com/@newlineauthors/abstract-equality-
operator-example4” />
We know from an earlier lesson that objects are truthy values. So, in the
above code example, it seems reasonable to assume that the if condition
would be evaluated as true, leading to the execution of the if block. But, if
you execute the above code, you might be surprised to know that instead of
the if block, the else would execute because the someVal == true check
would evaluate to false.
With the above condition, we will get the expected result because someVal
will be checked if it is a truthy value; if it is, the condition will evaluate to
true, leading to the execution of the if block.
So, as a piece of advice, avoid checks such as someVal == true, where one
operand is a boolean value. In such cases, take advantage of the implicit
coercive nature of the if statement, which will check if the value is a valid
value or not.
Further reading
Addition Operator
The addition operator can be used to perform the addition of two numbers,
or it can be used to join two strings, also known as string concatenation.
Relational operators
The relational operators (<, >, <=, >=) are used to compare both strings and
numbers. The abstract operation invoked in the case of relational operators
is IsLessThan abstract operation. Despite its name, this operation handles
“<=”, “>”, and “>=” comparisons as well.
We can also compare date objects using relational operators. Recall that the
Date objects are converted into strings when converted into primitive values
using the ToPrimitive operation with no preferred type, but in the case of
relational operators, Date objects, when converted into primitives, result in
a number representation of the Date objects because in the case of relational
operators, ToPrimitive abstract operation is passed “number” as the
preferred type.
1 const d1 = new Date("2022-11-03");
2 const d2 = new Date("2023-05-10");
3
4 console.log(d1 < d2); // true
<ReplitEmbed src=”https://replit.com/@newlineauthors/relational-
operators-example1” />
Now that we have had a deeper look at coercion and how some of the
common abstract operations work to make coercion work, let us test what
we learned in this module. This following exercise will allow us to
consolidate our understanding of the topic of coercion.
Following are some expressions that involve coercion. Try to guess the
output based on the knowledge you gained in this module. Don’t worry if
you don’t understand all of them. Their explanation is also given below.
You can obviously refer to the specification and the earlier lessons in this
module to understand and guess the output of the expressions below.
1 0 == false
2
3 "" == false
4
5 0 == []
6
7 [123] == 123
8
9 [1] < [2]
10
11 [] == ![]
12
13 !!"true" == !!"false"
14
15 [1, 2, 3] + [4, 5, 6]
16
17 [undefined] == 0
18
19 [[]] == ''
20
21 [] + {}
0 == false
Let’s start with an easy one, and most people will probably get it right, even
without reading this module. Now that we know how the abstract equality
operator works let us understand the steps taken to evaluate this expression
as true.
1. As the types are not equal and one of the operands is a boolean, the
boolean operand is converted into a number using the ToNumber
abstract operation. So, the first coercive step is to convert false into a
number, i.e., 0. The expression becomes:
1 0 == 0
"" == false
1. As the types are not equal and one of the operands is a boolean, the
boolean operand is converted into a number using the ToNumber
abstract operation. So, the first coercive step is to convert false into a
number, i.e., 0. The expression becomes:
1 "" == 0
2. Now, we have a string and a number. Recall that the abstract equality
operator prefers number comparison, so the string operand is
converted into a number using the ToNumber abstract operation. An
empty string, when converted into a number, outputs 0. So the
expression becomes:
1 0 == 0
0 == []
[123] == 123
:::info Weird fact about array conversion into a primitive value: an array
containing null or undefined is coerced into an empty string, i.e., [null]
—> "" and [undefined] —> "". Similarly, an array containing both of
them is coerced into a string containing a single comma, i.e., [null,
undefined] —> ",". Why don’t we get "null", "undefined", and
"null,undefined" for such arrays, respectively? This is just one of the
corner cases of coercion. :::
2. Now, we have two strings. The types are equal, so the strict equality
comparison is performed, i.e., "1" < "2", giving us true as an output
because the strings are compared using their Unicode code points.
[] == ![]
2. Next, the boolean operand, i.e., false is converted into a number, i.e.,
0. The expression is now:
1 [] == 0
3. Now we have a comparison between an object and a number.
Recalling how the abstract equality operator works, the object will be
converted into a primitive value, preferring the number type. As
mentioned in one of the earlier examples, an empty array is converted
into an empty string, so the expression becomes:
1 "" == 0
4. Next, the empty string is converted into a number, i.e., 0, using the
ToNumber abstract operation.
1 0 == 0
:::note If you are wondering how I know which operand, either left or right,
is coerced first and what coercion is performed, I am simply referring to the
steps mentioned in the ECMAScript specification. For example, for an
expression involving a comparison using the abstract equality operator, I am
referring to the steps of the IsLooselyEqual abstract operation.
!!"true" == !!"false"
[1, 2, 3] + [4, 5, 6]
[undefined] == 0
[[]] == ''
[] + {}
Some of the expressions above were taken from the following github repo:
wtfjs
Closures
Closure is one of the fundamental topics in JavaScript and can be tricky to
understand for beginners. It is a powerful feature, and developing a good
understanding of this topic is essential. In this module, we will take a
deeper look at what closures are and how they work.
What is a closure?
A Function
A reference to the environment/scope in which that function is created
<ReplitEmbed src=”https://replit.com/@newlineauthors/Example1”>
How does the innerFn, function returned by outerFn, have access to the
outerVar variable declared in outerFn even after the “outerFn” execution
is complete?
The above code works because JavaScript functions always create closures
when they are created. In some programming languages, a function’s
locally defined variables only exist for the duration of that function’s
execution; when a function’s execution ends, variables defined in its local
scope are destroyed. But that’s not the case in JavaScript, as is evident from
the code example above. So, how do closures work?
<ReplitEmbed src=”https://replit.com/@newlineauthors/Example2”>
Let us take a look at another code example that involves a nested function:
1 let isReading = true;
2
3 function learnJavaScript() {
4 function stepsToLearnJavaScript() {
5 console.log(isReading);
6 }
7
8 stepsToLearnJavaScript();
9 }
10
11 learnJavaScript();
<ReplitEmbed src=”https://replit.com/@newlineauthors/Example3”>
JavaScript will need to traverse the scope chain to find the declaration of
the isReading variable. As in the earlier example, the declaration is first
searched in the current scope, which in this case is the local scope of the
stepsToLearnJavaScript function. If the current scope doesn’t contain the
declaration JavaScript is looking for, the search is sequentially expanded to
the outer scopes. In our example, the stepsToLearnJavaScript function
doesn’t contain the declaration of the isReading variable, so the search is
moved to the outer scope, which in this case is the local scope of the
learnJavaScript function. The declaration doesn’t exist in this scope
either; JavaScript traverses to the outer scope of the learnJavaScript
function scope. The outer scope now is the global scope.
In the code example above, the variable declaration is found in the global
scope, so the search for the variable declaration will be stopped when the
search reaches the global scope. But what will happen if we remove the
variable declaration from the above code example?
1 function learnJavaScript() {
2 function stepsToLearnJavaScript() {
3 console.log(isReading);
4 }
5
6 stepsToLearnJavaScript();
7 }
8
9 learnJavaScript();
<ReplitEmbed src=”https://replit.com/@newlineauthors/Example4”>
At this point, we know how the scope is resolved, and the different scopes
are linked together, forming a chain that is known as the “scope chain.” But
have you wondered how different scopes are linked? It can’t be magic;
some mechanism must link different scopes together. How does JavaScript
determine the outer scope of the current scope?
Let us revisit one of the code examples presented earlier in this lesson:
1 let isReading = true;
2
3 function learnJavaScript() {
4 console.log(isReading);
5 }
6
7 learnJavaScript();
:::info
:::
Let’s revisit one of the earlier examples from this lesson that involved a
nested function:
1 let isReading = true;
2
3 function learnJavaScript() {
4 function stepsToLearnJavaScript() {
5 console.log(isReading);
6 }
7
8 stepsToLearnJavaScript();
9 }
10
11 learnJavaScript();
<ReplitEmbed src=”https://replit.com/@newlineauthors/Example6”>
The linkage between different scopes can be visualized in the image below:
scope linkage
Hopefully, the above two code examples and images have clarified how
different scopes are linked together, forming a scope chain. This scope
chain is traversed by the JavaScript engine, if needed, to resolve the scope
of any identifier.
This linkage between different environments, i.e., the scope chain, is what
makes closures possible in JavaScript. Due to this scope chain, a nested
function can still access variables from the outer function even after the
execution of the outer function has ended. This outer environment is kept in
memory as long as the inner function has a reference to it.
Now, let us revisit the first code example in this lesson, which involves
invoking a nested function from a different scope than the one it is defined
in.
1 function outerFn() {
2 const outerVar = 123;
3
4 return function inner() {
5 console.log(outerVar);
6 };
7 }
8
9 const innerFn = outerFn();
10 innerFn();
<ReplitEmbed src=”https://replit.com/@newlineauthors/Example7”>
Hopefully, you can now explain how the nested function has access to the
outerVar variable, even after the completion of the outerFn execution.
The inner function has a reference to the local scope of the outerFn
function, saved in its [[Environment]] internal slot. When the inner
function is returned from the outerFn function, although the outerFn
execution has ended, the inner function still has a reference to the local
scope of the outerFn. When the inner function is invoked, the value of the
[[Environment]] slot of the inner function is saved in the [[OuterEnv]]
internal slot of the environment created for the execution of the inner
function.
Most functions are usually invoked from the same scope in which they are
defined. This makes the closures go unnoticed. It is only when a function is
invoked from a different scope than the one it is defined in that closures
become noticeable.
In the following code example, a function is defined and invoked from the
global scope, so a closure formed by the function is unnoticeable.
1 let score = 150;
2
3 function logScore() {
4 console.log(score);
5 }
6
7 logScore();
<ReplitEmbed src=”https://replit.com/@newlineauthors/closure-
misconception-example1”>
Unlike the code example above, in the following code example, closure is
noticeable as the nested greet function has access to the greetMsg
parameter of the containing createGreeting function even after the
completion of the createGreeting function. The closure is noticeable
because the greet function is invoked from a different scope than the one it
is defined in. The greet function is defined in the local scope of the
createGreeting function, but it is invoked from the global scope.
1 function createGreeting(greetMsg) {
2 function greet(personName) {
3 console.log(`${greetMsg} ${personName}!`);
4 }
5
6 return greet;
7 }
8
9 const sayHello = createGreeting("Hello");
10 sayHello("Mike");
<ReplitEmbed src=”https://replit.com/@newlineauthors/closure-
misconception-example2”>
Although the above output seems reasonable, it is not the output we get
from the code example above. The actual output is as shown below:
1 4;
2 4;
3 4;
<ReplitEmbed src=”https://replit.com/@newlineauthors/closures-in-loops-
example1”>
If this output surprised you, then this lesson is for you. Even if it didn’t, do
you understand the reasons behind this output? In this lesson, we will
explore what causes this problem, also known as the “closures in loop”
problem, and how we can fix it.
The code example above suffers from this infamous “closures in loop”
problem because the callback function of each setTimeout forms a closure
over the same variable i. As there are a total of three loop iterations in our
example, setTimeout is called three times, so we have three callback
functions, all having a closure over the same variable i.
The callback function of each setTimeout call is invoked after the loop
execution has completed. The value of variable i after the last iteration of
the loop is “4”, and because each callback function of the “setTimeout” has
a closure over the same variable i, all three of them see “4” as the value of
the i. This is the reason they all log “4” on the console.
The scope linkage for the code example above can be visualized in the
image below:
scope linkage
It is important to note that functions form closures over variables, not their
values. Closure over variables implies that each function logs the latest
value of the variable it has closed over; if functions formed closure over
values rather than variables, they would log the snapshot of the value in
effect when the closure was formed.
In our example, if the closure was over the values of i instead of the i
variable, then each callback would have logged the value of i that was in
effect in the iteration in which that callback was created. This means we
would have gotten the expected output, i.e., 1 2 3 instead of 4 4 4. But as
is evident from the output of the code example, closures are formed over
variables. As a result, each callback function of setTimeout has a closure
over the variable i, and when each callback is executed, it sees the latest
value of i.
Pre-ES2015 solution
Before the introduction of ES2015, also known as ES6, one way to solve
this problem was to use an IIFE (Immediately Invoked Function
Expression). The following code example shows how using an IIFE
resolves this problem.
1 for (var i = 1; i <= 3; i++) {
2 ((counter) => {
3 setTimeout(() => {
4 console.log(counter);
5 }, 1000);
6 })(i);
7 }
8
9 /* output
10 ---------
11 1
12 2
13 3
14 ---------
15 */
<ReplitEmbed src=”https://replit.com/@newlineauthors/closures-in-loops-
example2”>
Recall that the problem is caused by the closure of different callbacks over
the same variable. But with the use of an IIFE, we can pass the value of i in
each iteration to the IIFE as a parameter. This parameter (counter) is then
used inside the callback function of the setTimeout function. This solves
the problem because the counter parameter is closed over by each callback
function, and in each iteration, a new IIFE is created, along with the new
callback function of setTimeout. Each new instance of the IIFE gets passed
a new value of i, i.e., “1” in the first iteration, “2” in the second iteration,
and so on. So now, with the use of an IIFE, each callback function has a
closure over a different counter variable.
The image below helps visualize the scope chain in the above code example
(function objects are not shown in the image below to keep it simple):
scope linkage
The output of the code example above can be seen in the Replit below:
<ReplitEmbed src=”https://replit.com/@newlineauthors/closures-in-loops-
example3”>
ES2015 solution
<ReplitEmbed src=”https://replit.com/@newlineauthors/closures-in-loops-
example4”>
Using the let keyword solves this problem because, unlike each callback
function closing over the same variable i, the let being block-scoped
causes each iteration of the loop to have a different copy of the variable i.
This is the key idea that solves the problem of “closures in loop”. Each
iteration has its own separate copy of variable i, which means that the
setTimeout callback created in each iteration closes over the copy of
variable i that is limited to that particular iteration of the loop.
In our code example, we have three iterations of the loop and separate
copies of variable i, each limited to a particular iteration of the loop.
Although it seems that we have a single variable i, behind the scenes, each
iteration gets its own copy of the variable i.
To understand how each iteration gets its own copy of variable i, the
following steps explain how the above code is executed:
Hopefully, the final diagram clarifies why using the let keyword solves the
“closures in loop” problem. As each environment object created for each
iteration of the loop has its separate copy of the variable i, the closure of
each callback created in different iterations of the loop forms a closure over
a separate copy of the variable i. As a result, they log the value they have
closed over, giving us the expected output, i.e., 1 2 3.
The code inside the IIFE is executed, and an object is returned from the
IIFE that is assigned to the bank variable. The object returned only contains
the data that needs to be accessible from outside the IIFE. The code that is
meant to be private is not returned; as a result, that data remains private,
limited to the boundary walls of the IIFE. However, thanks to closures,
publicly exposed data that is returned from the IIFE can still access the
private data inside the IIFE.
Further reading
The prototype chain allows the sharing of properties between objects, and
this is the idea of inheritance in JavaScript, known as the “prototypal
inheritance”. In prototypal inheritance, an object from which other objects
inherit properties is known as the “prototype” of those objects.
In the code example above, the [[Prototype]] slot of the obj object
contains a reference to the Object.prototype object. So, obj.
[[Prototype]] gives us the prototype of the obj object, the object from
which obj is linked to and inherits the properties. But as [[Prototype]] is
an internal slot not accessible by JavaScript, later in this lesson, we will see
how we can access the prototype of any object.
As functions are objects in JavaScript, they can have properties just like any
other object. The property name “prototype” is one of the functions’
properties. The arrow functions do not have this property.
The following code example shows that the property named “prototype”
exists on functions:
1 function Car(name, model) {
2 this.name = name;
3 this.model = model;
4 }
5
6 console.log(Object.getOwnPropertyNames(Car));
7
8 // [ "prototype", "length", "name" ]
<ReplitEmbed src=”https://replit.com/@newlineauthors/prototypal-
inheritance-example1” />
You can probably tell from the code above that the Car function is meant to
be used as a constructor function. However, it is indeed just a normal
function. The “prototype” property is only useful when we invoke a
function as a constructor, i.e., with the new keyword.
Any properties added to the Car.prototype object will be shared by all the
instances created from the Car constructor function. The Car.prototype
function will serve as the “prototype” for all the instances of the Car
constructor function.
Initially, the object pointed to by the prototype property on any function just
contains a single property named “constructor”. The value of this
“constructor” property is a reference to the constructor function. In the case
of Car.prototype object, Car.prototype.constructor refers to the Car
constructor function.
1 // Car.prototype
2 {
3 constructor: <Car function>
4 }
:::note
The constructor property is rarely used, if at all, in the JavaScript code that
we write.
:::
<ReplitEmbed src=”https://replit.com/@newlineauthors/prototypal-
inheritance-example3” />
When a function is invoked using the new keyword, one of the steps during
the creation of a new object is that the [[Prototype]] internal slot of the
newly created object is pointed to the object referenced by the function’s
prototype property. As a result, the newly created object has access to the
properties defined on the object referred to by the constructor function’s
prototype property.
The Object function has a static method named getPrototypeOf that can be
used to get the prototype of any object. It returns the value of the internal
[[Prototype]] property of the object.
For the honda object created in the previous code example,
Object.getPrototypeOf function returns the Car.prototype object
because the Car.prototype object is the prototype of all the instances of
the Car constructor function.
1 function Car(name, model) {
2 this.name = name;
3 this.model = model;
4 }
5
6 Car.prototype.start = function () {
7 console.log("starting the engine of " + this.name);
8 };
9
10 const honda = new Car("honda", "1996");
11
12 console.log(Object.getPrototypeOf(honda) === Car.prototype); // true
<ReplitEmbed src=”https://replit.com/@newlineauthors/prototypal-
inheritance-example4” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/prototype-chain-
example1” />
We didn’t define any properties on the obj object. But we can still call some
methods on it.
1 const obj = {};
2
3 console.log(obj.toString()); // [object Object]
<ReplitEmbed src=”https://replit.com/@newlineauthors/prototype-chain-
example2” />
Objects created in this way are instances of the Object constructor function.
We can also define obj as shown below:
1 const obj = new Object();
This has the same effect: it creates an empty object. As discussed in the
previous lesson, functions have a prototype property that points to an object
that serves as the “prototype” of all instances of that function when that
function is invoked as a “constructor”. So, the Object.prototype object
serves as the “prototype” of all objects created via new Object() or through
object literal notation.
At this point, you might ask: isn’t toString callable on all objects? Yes, it
is; some objects inherit it from the Object.prototype object, while other
objects, such as arrays, inherit it from their prototype, i.e., the
Array.prototype object, which overrides the toString implementation
defined in Object.prototype.
<ReplitEmbed src=”https://replit.com/@newlineauthors/prototype-chain-
example3” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/prototype-chain-
example4” />
The prototype chain for strings is similar to that for arrays, except that
instead of the Array.prototype object, there is the String.prototype
object, which serves as the prototype of all string instances. This
String.prototype object is, in turn, linked to the Object.prototype
object, where the Object.prototype object serves as the prototype of the
String.prototype object.
“Function” function
<ReplitEmbed src=”https://replit.com/@newlineauthors/prototype-chain-
example5” />
Although this property can be used to set and get the prototype of an object,
its use is discouraged. This property has been deprecated, and better
alternatives have been provided to get and set the prototype of an object.
1 const user = { name: "John Doe" };
2
3 console.log(user.__proto__);
4 // logs Object.prototype object
<ReplitEmbed src=”https://replit.com/@newlineauthors/proto-property-
example1” />
The above code example shows the use of the __proto__ property as a
getter to get the prototype of the user object. It can also be used as a setter
to set the value of the [[Prototype]] internal property of an object. Again,
its use is discouraged, and there are better alternatives. For setting the
prototype, we can use the setPrototypeOf method.
The prototype of the user object in the code example above is the
Object.prototype object. As a result, the user object has access to the
__proto__ property. As the __proto__ property, when used as a getter,
simply exposes the value of the internal [[Prototype]] property of an
object, in the case of user, it returns the Object.prototype object.
There are multiple reasons to avoid using __proto__ property to get or set
the prototype of an object.
How can we use this object as a prototype of another object? We can use
the setPrototypeOf method:
1 const propertyPrinter = {
2 printOwnPropertyNames: function () {
3 // "this" refers to the object on which
4 // this function is called
5 for (let prop of Object.getOwnPropertyNames(this)) {
6 console.log(prop);
7 }
8 }
9 };
10
11 const user = {
12 firstName: "John",
13 lastName: "Doe",
14 age: 25
15 };
16
17 // set the prototype of the "user" object
18 Object.setPrototypeOf(user, propertyPrinter);
19
20 // prototype methods are now accessible
21 user.printOwnPropertyNames();
22 // firstName
23 // lastName
24 // age
<ReplitEmbed src=”https://replit.com/@newlineauthors/Custom-
prototypes-example2” />
The deprecated __proto__ property can also be used to achieve the same
result, but as it is deprecated, we will instead discuss another option to set
the prototype of an object explicitly.
Object.create method
<ReplitEmbed src=”https://replit.com/@newlineauthors/Custom-
prototypes-example3” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/Custom-
prototypes-example4” />
The obj object in the above example doesn’t have a prototype. If its
prototype wasn’t explicitly set to null, its prototype would have been the
Object.prototype object, and it would have inherited the toString
method, but as is evident from the code example above, obj doesn’t have
access to the toString method.
The null prototype objects may seem useless, but they are useful in some
cases. For example, such objects are safe from attacks such as the prototype
pollution attack, where a malicious code might add some properties to the
prototype chain of an object that could change the normal flow of code
execution.
If the prototype of the user object was null, malicious code wouldn’t have
an effect on our code.
1 const user = Object.create(null);
2
3 // malicious code adding "isAdmin"
4 // property in the prototype object
5 Object.prototype.isAdmin = true;
6
7 if (user.isAdmin) {
8 console.log("grant access");
9 } else {
10 console.log("access denied");
11 }
<ReplitEmbed src=”https://replit.com/@newlineauthors/Custom-
prototypes-example6” />
Until 2015, JavaScript didn’t have classes. Constructor functions were used
instead. To inherit from a constructor function, JavaScript developers
explicitly created a link between the prototype properties of two different
constructor functions by using the Object.create method. The following
code example shows how one constructor function could extend another
constructor function to reuse some functionality:
1 function Person(name, age) {
2 this.name = name;
3 this.age = age;
4 }
5
6 Person.prototype.introduce = function () {
7 console.log(`My name is ${this.name} and I am ${this.age} years old`);
8 };
9
10 function Student(name, age, id) {
11 // delegate the responsibility of initializing
12 // "name" and "age" properties to the Person
13 // constructor
14 Person.call(this, name, age);
15 this.id = id;
16 }
17
18 // set "Person.prototype" object as the prototype
19 // of the "Student.prototype" object
20 Student.prototype = Object.create(Person.prototype);
21
22 // set the constructor property on the
23 // newly created Student.prototype object
24 Student.prototype.constructor = Student;
25
26 const mike = new Student("Mike", 20, 222);
27 mike.introduce();
<ReplitEmbed src=”https://replit.com/@newlineauthors/ES6-classes-and-
prototypes-example1” />
The following three points are worth noting in the code example above:
The image below helps visualize the prototype chain created as a result of
our code example:
constructor function prototype chain
Although the code above works, it is error-prone because there are multiple
steps to set a prototype link correctly between the two constructors. Imagine
having more than two constructors that need to be linked like this. It is easy
to forget any steps necessary to set up the prototype link correctly. Ideally,
we want a more declarative way of achieving the same result. Ideally, we
want a more declarative way of achieving the same result. A declarative
solution will allow us to get the same result without having to explicitly
create a link between the Student.prototype and Person.prototype
objects.
ES2015 classes
The code above gives us the same result as the one with the constructor
functions. It also creates the same prototype linkages. We can verify this
with the following comparisons:
1 console.log(Object.getPrototypeOf(mike) === Student.prototype);
2 // true
3 console.log(Object.getPrototypeOf(Student.prototype) === Person.prototype);
4 // true
5 console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype);
6 // true
One important thing to mention here is that the classes are just syntactic
sugar over the traditional constructor functions. Under the hood, we are still
using the constructor functions, but classes allow us to write the code in a
more declarative way.
One extra thing that the extends keyword does is that, apart from setting
the linkage between Student.prototype and Person.prototype objects, it
also links the constructor functions. It does this by setting the Person class
as the prototype of the Student class. The following code verifies the
second prototype chain that the extends keyword sets up for us behind the
scenes.
1 console.log(Object.getPrototypeOf(Student) === Person);
2 // true
The two prototype chains set up by the extends keyword serve two
different purposes:
Function context
The this keyword is mostly used inside functions to refer to the object
using which the function was invoked. In other words, when a function is
invoked as a “method” (invoked using an object), the this keyword
becomes applicable for referencing the object used to invoke the function.
The printInfo function in the code example above uses the this keyword,
and looking at the code, we can tell that the printInfo function assumes
that the value of this inside the printInfo function will be an object with
three properties: id, name, and email. But as mentioned earlier, the value of
this inside a function depends on how the function is called.
In the code example above, the printInfo function is invoked using the
student object, and when a function is invoked using an object, the this
inside that function refers to the object using which the function was
invoked. So in our code example, this inside the printInfo refers to the
student object. As the student object has the three properties that are
accessed using the this keyword inside the printInfo function, their
values are logged to the console.
<ReplitEmbed src=”https://replit.com/@newlineauthors/what-is-this-
example2”>
The answer to the question above depends on whether our code is executed
in strict mode. If non-strict mode, this inside a function, when not invoked
as a method, refers to the global object, which in the case of browsers is the
window object. However, the value of this inside a function is undefined in
strict mode when not invoked as a method.
Can you guess in which mode the code was executed from the output of the
code above? As this.fullName evaluated to undefined, the code was
executed in non-strict mode.
<ReplitEmbed src=”https://replit.com/@newlineauthors/what-is-this-
example3”>
An error? Why?
Global context
In the global scope, the value of this depends on the environment in which
our JavaScript code is executed.
:::info
:::
Inside web workers, the value of this at the top level refers to the global
scope of the web worker, which is different from the global scope
containing the window object in the browser. Code inside a web worker is
executed in its own separate context with its own global scope.
The function above, when invoked as a constructor function, will add two
properties: name and ingredients to the newly created object.
Class context
<ReplitEmbed src=”https://replit.com/@newlineauthors/what-is-this-
example5”>
The code example above throws an error because we have invoked the
printColor method as a “function”. As mentioned earlier, code inside a
class executes in strict mode, so, as within functions in strict mode, this
inside methods is undefined.
We already know that this inside a function depends on how the function is
called. But what about the callback functions that we do not call? I mean
the callbacks like the DOM event handlers that we do not call but instead
are called for us by JavaScript whenever the click event is triggered. In such
cases, what is the value of this?
The event listener callback is invoked with this set to the HTML element
that triggered the event. Consider the following code example:
1 <button>Submit</button>
2
3 <script>
4 const btn = document.querySelector("button");
5
6 class FormHandler {
7 constructor(submitBtn) {
8 submitBtn.addEventListener("click", this.submitForm);
9 }
10
11 submitForm() {
12 console.log("form submitted");
13 console.log(this);
14 }
15 }
16
17 new FormHandler(btn);
18 </script>
<ReplitEmbed src=”https://replit.com/@newlineauthors/what-is-this-
example6”>
Run the above code in a browser and check the console in the browser
developer tools. Specifically, note the value of this logged by the
submitForm method. The value of this inside this method, when it is
invoked as an event listener callback, is the button element and not the
instance of the class like we would normally expect.
This can cause a problem if we are not careful when using this inside an
event listener callback function. Imagine a scenario where we had to call
another method within the FormHandler class:
1 <button>Submit</button>
2
3 <script>
4 const btn = document.querySelector("button");
5
6 class FormHandler {
7 constructor(submitBtn) {
8 submitBtn.addEventListener("click", this.submitForm);
9 }
10
11 submitForm() {
12 this.sendRequest();
13 // ERROR: this.sendRequest is not a function
14 }
15
16 sendRequest() {
17 console.log("sending request...");
18 }
19 }
20
21 new FormHandler(btn);
22 </script>
Before diving into how this works inside arrow functions, let us first
explore the problem with using the this keyword inside regular functions.
Consider the following code example:
1 function Counter(startingValue) {
2 this.value = startingValue;
3 }
4
5 Counter.prototype.incrementFactory = function (incrementStep) {
6 return function () {
7 this.value += incrementStep;
8 console.log(this.value);
9 };
10 };
11
12 const counter = new Counter(0);
13 const increment5 = counter.incrementFactory(5);
14 increment5(); // NaN
15 increment5(); // NaN
16 increment5(); // NaN
<ReplitEmbed src=”https://replit.com/@newlineauthors/arrow-functions-
and-this-example1” />
Why did we get NaN as an output? The reason for unintended output is the
incorrect value of this inside the function returned from the
incrementFactory function.
Recall how the value of this gets set inside a function. It depends on how
the function is called. In the code example above, how is increment5
function called? Is it called as a “method” or as a standalone function? It is
called a “function”, so the value of this depends on whether our code is in
strict mode or not. Assuming that our code is in non-strict mode, the value
of this inside the increment5 function is the global object, i.e., the window
object in the case of browsers. So, this.value is actually window.value,
and it is undefined because the window object, by default, doesn’t have a
value property. As a result, we get the NaN value when undefined is added
to a number, i.e., the value of the incrementStep parameter.
How can we fix this problem? How can we ensure that the value of this
inside the increment5 function is what we want it to be? There are multiple
ways to handle this problem. One way is to save the value of this inside
the incrementFactory function before returning a function, and inside the
returned function, use the variable containing the value of this instead of
directly using this. The following code example shows this approach in
action:
1 function Counter(startingValue) {
2 this.value = startingValue;
3 }
4
5 Counter.prototype.incrementFactory = function (incrementStep) {
6 const thisVal = this; // save `this` value
7 return function () {
8 // use `thisVar` variable instead of `this`
9 thisVal.value += incrementStep;
10 console.log(thisVal.value);
11 };
12 };
13
14 const counter = new Counter(0);
15 const increment5 = counter.incrementFactory(5);
16 increment5(); // 5
17 increment5(); // 10
18 increment5(); // 15
<ReplitEmbed src=”https://replit.com/@newlineauthors/arrow-functions-
and-this-example2” />
The approach shown above was commonly used to fix similar problems
where the value of this from the surrounding context was needed instead
of the one from the current function where this was actually used. In the
code example above, we needed the value of this from the surrounding
context of the incrementFactory function and not the one inside the
increment5 function.
Arrow functions to the rescue
Another way to solve the problem shown above is to use an arrow function.
Let’s change the code example above to use an arrow function:
1 function Counter(startingValue) {
2 this.value = startingValue;
3 }
4
5 Counter.prototype.incrementFactory = function (incrementStep) {
6 // use an arrow function
7 return () => {
8 this.value += incrementStep;
9 console.log(this.value);
10 };
11 };
12
13 const counter = new Counter(0);
14 const increment5 = counter.incrementFactory(5);
15 increment5(); // 5
16 increment5(); // 10
17 increment5(); // 15
<ReplitEmbed src=”https://replit.com/@newlineauthors/arrow-functions-
and-this-example3” />
Clicking the submit button in the code example above throws an error
because the value of this inside the submitForm method is the button
element instead of the instance of the FormHandler class. As a result, the
this.sendRequest() call throws an error because this needs to refer to an
instance of the FormHandler class to allow us to call other methods in this
class from within the submitForm method. So the problem is, how can we
call the sendRequest method from the submitForm method? We said in the
previous lesson that there is more than one way to solve this problem. One
of them is to use an arrow function.
To fix the issue, inside the constructor of the FormHandler class, we can
pass an arrow function instead of this.submitForm as a callback function
to the click event listener. Inside the arrow function, we can invoke the
submitForm method to handle the click event.
1 class FormHandler {
2 constructor(submitBtn) {
3 submitBtn.addEventListener("click", () => this.submitForm());
4 }
5
6 // methods...
7 }
Why did passing an arrow function as a callback fix the issue? We are still
invoking the submitForm method inside the arrow function, so how is this
different from directly passing this.submitForm as a callback function?
The reason an arrow function fixed the issue is that, as discussed earlier,
arrow functions do not have their own value of this; they get it from the
surrounding environment. The surrounding environment is the constructor
in this case. What’s the value of this inside the constructor? Its value is an
instance of the FormHandler class when the constructor is invoked using the
new keyword. So, instead of this referring to an HTML element inside the
event handler callback function like it did in the previous example, the
value of this inside the arrow function is the same as in the constructor,
i.e., an instance of the FormHandler class. So when we invoke the
submitForm method inside the arrow function, the value of this inside the
submitForm method is also an instance of the FormHandler class. As a
result, we can call any other method from inside the submitForm method.
In the older version of this code that throws an error, the event handler was
this.submitForm method. It was not invoked by our code explicitly.
Instead, it is invoked by JavaScript whenever the submit button is clicked.
We know that the value of this inside functions depends on how a function
is called. In this case, as we weren’t invoking the function explicitly, we
couldn’t control the value of this inside the submitForm method. Using an
arrow function allowed us to invoke the submitForm method explicitly and,
consequently, allowed us to control the value of this inside it.
Arrow functions are really useful and a welcome addition to the JavaScript
language. The problems they solve can also be solved in other ways, but
other solutions are more verbose than arrow functions.
So far, we have discussed that the value of this depends either on the
environment in which our JavaScript code is executed or, in the case of
functions, on how a function is called. We have also discussed that arrow
functions don’t have their own value of this; instead, they get their value
from the surrounding context.
All the ways we have seen so far for setting the value of this automatically
set its value. Javascript also provides us with ways to explicitly set this to
whatever value we want.
We can use any of the following three built-in methods to explicitly set the
value of this:
Function.prototype.call()
Function.prototype.apply()
Function.prototype.bind()
We won’t go into details of how these methods work; you can learn how
each of these methods works using the links given above. We will, however,
see how explicitly setting this can be useful.
Let us revisit the code example from the previous lesson about arrow
functions:
1 function Counter(startingValue) {
2 this.value = startingValue;
3 }
4
5 Counter.prototype.incrementFactory = function (incrementStep) {
6 return function () {
7 this.value += incrementStep;
8 console.log(this.value);
9 };
10 };
11
12 const counter = new Counter(0);
13 const increment5 = counter.incrementFactory(5);
14 increment5(); // NaN
15 increment5(); // NaN
16 increment5(); // NaN
<ReplitEmbed src=”https://replit.com/@newlineauthors/binding-this-
example1” />
In the previous lesson, we saw how using an arrow function fixed the code
example above. We could also fix this code by explicitly setting the value of
this to the desired value, i.e., the counter object, which is used to invoke
the incrementFactory function. Instead of using an arrow function, we
could use the bind method to set the value of this.
1 function Counter(startingValue) {
2 this.value = startingValue;
3 }
4
5 Counter.prototype.incrementFactory = function (incrementStep) {
6 const incrementFn = function () {
7 this.value += incrementStep;
8 console.log(this.value);
9 };
10
11 // return a function with `this` bound
12 // to the object used to invoke the
13 // `incrementFactory` method
14 return incrementFn.bind(this);
15 };
16
17 const counter = new Counter(0);
18 const increment5 = counter.incrementFactory(5);
19 increment5(); // 5
20 increment5(); // 10
21 increment5(); // 15
<ReplitEmbed src=”https://replit.com/@newlineauthors/binding-this-
example2” />
Borrowing methods
Imagine having an object that contains methods that can be useful for other
objects as well. How can we use those methods with other objects? One
option is to duplicate the definition of methods for each object that needs
them. But we don’t want duplication. Is there a way to avoid duplication
and reuse the existing methods?
1 const john = {
2 name: "John",
3 sayHello() {
4 console.log("Hello, I am " + this.name);
5 }
6 };
7
8 const sarah = {
9 name: "Sarah"
10 };
11
12 // borrow method from john
13 const sayHello = john.sayHello;
14 sayHello.call(sarah);
15 // Hello, I am Sarah
You can run the above code in the Replit below:
<ReplitEmbed src=”https://replit.com/@newlineauthors/binding-this-
example3” />
We can explicitly set the value of this inside a function and use it with
other objects using any of the three methods mentioned above. This allows
us to avoid duplication and reuse code.
At this point, you might ask: can we not create a constructor that allows us
to create objects and add common methods in the constructor prototype
property? You are right. Creating a constructor is the right way to handle a
situation where we want to create similar objects. However, explicitly
setting the value of this allows us to reuse code between unrelated objects.
It is a nice option to have and can be used where appropriate.
In the code example above, the call method has been used to call the
Employee constructor, passing in the three properties that the Employee
constructor can set on the newly created object. But how can we tell the
Employee constructor to add the properties to the newly created
BankEmployee object? This is where the first argument passed to the call
method comes in. We have passed this as the first argument. Recall how
the value of this is set inside a function: it depends on how the function is
called. In this case, we expect the BankEmployee function to be invoked as a
constructor function using the new keyword. As a result, this inside the
BankEmployee function will be the newly created object. This newly created
object is explicitly set as the value of this inside the Employee constructor.
The problem we are trying to fix is that we want to call other methods of
the same class from inside the event handler callback, but trying to do so
throws an error because this inside an event handler is the HTML element
that triggered the DOM event. In the previous lesson, we saw how an arrow
function could solve this problem. There’s another way to fix the issue, and
that’s to explicitly set the value of this inside the submitForm method.
1 class FormHandler {
2 constructor(submitBtn) {
3 submitBtn.addEventListener("click", this.submitForm.bind(this));
4 }
5
6 // methods...
7 }
Explicitly setting the value of this inside the submitForm method using the
bind method fixes the problem because it overrides the default value of
this inside an event callback function. We have explicitly set it to the value
of this inside the FormHandler class constructor, i.e., an instance of the
FormHandler class.
Imagine a scenario where you want to check if the global object contains a
certain property regardless of whether your code is executed in a browser or
a NodeJS environment. Without a standard way to access the global object,
you might write the following code:
1 if (typeof window !== "undefined" && window.secretProperty) {
2 // execute code for browser
3 } else if (typeof global !== "undefined" && global.secretProperty) {
4 // execute code for nodejs
5 }
“this” vs globalThis
The globalThis should not be confused with the this keyword. We have
discussed in this module how the value of this can vary depending on
different execution contexts, but globalThis is just a standard way to
access the global object in different JavaScript environments. Its value only
varies depending on the environment in which our code is executed. It isn’t
affected by how the function is called or whether our code is in strict mode
or not.
In the case of an arrow function, the value of this is taken from the
surrounding context.
In the case of a regular function, the value of this depends on how a
function is called and whether the code is executed in strict mode or
not.
If a function is invoked as a constructor using the new keyword,
the value of this is the newly created object.
If the value of this is explicitly set using bind, call, or apply
functions, then the value of this is whatever value is passed as
the first argument to these functions.
If a function is invoked as a “method”, the value of this is the
object used to call the method.
If the function is invoked without any object, i.e., as a “function”,
the value of this is the global object in non-strict mode and
undefined in strict mode.
In DOM event handler callbacks, the value of this is the HTML
element that triggered the event.
In the global scope in browsers, this refers to the global window
object.
In NodeJS, code at the top level is executed in a module scope. In
ECMAScript modules, this is undefined at the top level of a module
because the code in the ECMAScript module is implicitly executed in
strict mode. In CommonJS modules, this refers to the
module.exports object at the top level of a module.
Before we discuss how to use symbols in our code, let us first understand
the motivation behind adding symbols to the JavaScript language.
It turned out that each symbol being a unique value is pretty useful because
it allows the JavaScript language to be extended and remain backwards
compatible. Symbols allow JavaScript to add new properties to objects that
cannot conflict with the existing properties on objects that others might
have used in their code.
Think about how such a feature could have been added to the language with
a string property. What name could possibly have been chosen that was
guaranteed to not break existing code?
Symbol values can be created using the Symbol function. It’s important to
note that the Symbol function must be invoked without the new keyword.
Attempting to use the new keyword to invoke the Symbol function will
result in an error. This is because it prevents the creation of an object
wrapper around the symbol. Every call to the Symbol function must create a
new unique symbol value.
1 const sym = Symbol();
<ReplitEmbed src=”https://replit.com/@newlineauthors/creating-symbols-
example2” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/creating-symbols-
example3” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/creating-symbols-
example4” />
In this example, a property key “name” was added to the person object
using the computed property name. This property can be accessed in
multiple ways, as demonstrated in the code example above. But what
happens if the value of the name variable is changed from a string to a
symbol?
1 const name = Symbol();
2
3 const person = {
4 [name]: "John Doe"
5 };
6
7 console.log(person.name); // undefined
8 console.log(person["name"]); // undefined
9 console.log(person[name]); // John Doe
<ReplitEmbed src=”https://replit.com/@newlineauthors/creating-symbols-
example5” />
So, if the original symbol is not accessible, does that make the symbol
property a private property? What happens if the properties of an object are
iterated over? Let’s find out if the symbol properties of an object can be
obtained.
1 const name = Symbol();
2
3 const person = {
4 [name]: "John Doe",
5 age: 20
6 };
7
8 // only sees the "age" property
9 for (const prop in person) {
10 console.log(prop);
11 }
12
13 console.log(Object.keys(person)); // ["age"]
14
15 console.log(Object.getOwnPropertyNames(person)); // ["age"]
<ReplitEmbed src=”https://replit.com/@newlineauthors/creating-symbols-
example6” />
Looking at the output of the above code example, it might be assumed that
symbol properties are indeed private. However, this assumption would be
incorrect. Symbol properties are not private, as the next code example
demonstrates.
1 const name = Symbol();
2
3 const person = {
4 [name]: "John Doe",
5 age: 20
6 };
7
8 console.log(Object.getOwnPropertyDescriptors(person));
9
10 console.log(Object.getOwnPropertySymbols(person));
<ReplitEmbed src=”https://replit.com/@newlineauthors/creating-symbols-
example7” />
The above code example shows that symbol properties can be discovered
using methods like Object.getOwnPropertyDescriptors or
Object.getOwnPropertySymbols. While symbol properties may be a bit
more inconvenient to access compared to string properties, they are not
private. JavaScript has true private properties, and symbols are not intended
to be used as private properties.
When creating symbols, there is the option to provide a description for each
symbol. This description can be useful for debugging purposes. The
following code example creates a symbol with a description:
1 const propSymbol = Symbol("property symbol");
<ReplitEmbed src=”https://replit.com/@newlineauthors/creating-symbols-
example9” />
The description property can only be used to get the property of a symbol;
it cannot be used to set the description. The following code example shows
that assigning a value to the description property of a symbol doesn’t
change its value. The description property is actually a getter, and a setter
isn’t defined for this property. As a result, the description can only be
obtained but not set using this property.
1 const propSymbol = Symbol("property symbol");
2
3 console.log(propSymbol.description);
4 // property symbol
5
6 propSymbol.description = "123";
7
8 console.log(propSymbol.description);
9 // property symbol
<ReplitEmbed src=”https://replit.com/@newlineauthors/creating-symbols-
example10” />
Each call to the Symbol function creates a new unique symbol. However,
JavaScript also allows us to create symbols that can be shared across files or
realms. This is where the concept of a global symbol repository comes in.
We can use the Symbol.for method to create a global symbol in the global
symbol repository.
1 const globalSymbolKey = "my-global-Symbol";
2 const mySymbol = Symbol.for(globalSymbolKey);
3
4 console.log(mySymbol === Symbol.for(globalSymbolKey));
5 // true
<ReplitEmbed src=”https://replit.com/@newlineauthors/global-symbols-
example” />
We pass a key to Symbol.for method, and using this key, we can retrieve
the symbol associated with it in the global symbol repository. If the key
exists in the repository, the Symbol.for method returns the symbol
associated with it; otherwise, it creates a new symbol in the repository and
associates it with the given key.
If we have a global symbol, we can get the key it is associated with using
the Symbol.keyFor method.
1 const globalSymbolKey = "my-global-Symbol";
2 const mySymbol = Symbol.for(globalSymbolKey);
3
4 console.log(Symbol.keyFor(mySymbol));
5 // my-global-Symbol
<ReplitEmbed src=”https://replit.com/@newlineauthors/global-symbols-
example2” />
The following code example hooks into the object to the primitive
conversion process of the movie object and returns a different
representation of the object based on the value of the hint argument.
1 const movie = {
2 name: "Jurassic Park",
3 releaseDate: "09,June,1993",
4
5 [Symbol.toPrimitive](hint) {
6 if (hint === "number") {
7 return new Date(this.releaseDate).getTime();
8 } else {
9 return this.name;
10 }
11 }
12 };
13
14 console.log(Number(movie));
15 console.log(String(movie));
You can see the output of the above code example in this Replit:
<ReplitEmbed src=”https://replit.com/@newlineauthors/well-known-
symbols-example1” />
Symbol.toStringTag
You can see the output of the above code example in this Replit:
<ReplitEmbed src=”https://replit.com/@newlineauthors/well-known-
symbols-example3” />
You can see the output of the above code example in this Replit:
<ReplitEmbed src=”https://replit.com/@newlineauthors/well-known-
symbols-example4” />
Symbol.isConcatSpreadable
You can see the output of the above code example in this Replit:
<ReplitEmbed src=”https://replit.com/@newlineauthors/well-known-
symbols-example5” />
As the output of the above code example shows, the default behavior for
arrays is to spread their elements. This default behavior can be overridden
by setting Symbol.isConcatSpreadable to false.
For array-like objects, the default behavior is to not spread their properties.
This can be overridden by setting Symbol.isConcatSpreadable to true.
1 const obj = {
2 0: 123,
3 1: 456,
4 length: 2,
5 [Symbol.isConcatSpreadable]: true
6 };
7
8 console.log([].concat(obj));
9 // [123, 456]
You can see the output of the above code example in this Replit:
<ReplitEmbed src=”https://replit.com/@newlineauthors/well-known-
symbols-example6” />
There are other well-known symbols that allow us to hook into different
built-in operations in JavaScript. The complete list can be seen in the
ECMAScript specification or MDN - Symbol.
Asynchronous JavaScript
In this module, we will cover asynchronous programming in JavaScript. We
will learn what asynchronous programming means and how it was
traditionally done in JavaScript. We will also discuss the problems with the
traditional way of handling asynchronous code in JavaScript and how
promises, introduced in ES2015, changed the way we handle asynchronous
code in JavaScript. We will discuss promises in detail and also learn about
the async-await syntax that simplifies using promises.
<ReplitEmbed src=”https://replit.com/@newlineauthors/overview-
example1”>
<ReplitEmbed src=”https://replit.com/@newlineauthors/overview-
example2”>
On the initial page load, you will notice that the button is not clickable for a
few seconds. The UI will stay frozen until the JavaScript code has been
executed, specifically the long-running loop.
:::info
:::
Operations like HTTP requests are asynchronous, but they aren’t handled
by JavaScript. The code we write initiates the asynchronous operation; the
actual asynchronous operation is handled by the browser in the case of
client-side JavaScript, background threads, or the operating system itself in
the case of the NodeJS runtime.
This is how JavaScript gets around its limitation of a single thread. The
asynchronous operations are actually handled by the runtime (browser,
NodeJS, etc.). In the meantime, JavaScript can do other things.
<ReplitEmbed src=”https://replit.com/@newlineauthors/callbacks-
example1” />
Different DOM events, for example, the click event, are also handled
asynchronously using the callback functions. A callback is registered as an
event handler and is invoked later whenever the event is triggered.
1 const submitBtn = document.getElementById("submit");
2
3 submitBtn.addEventListener("click", function (event) {
4 // code to handle the click event
5 });
Similarly, different timer functions like setTimeout are also provided with
a callback function that is intended to be invoked after the specified amount
of time has elapsed.
1 setTimeout(function () {
2 console.log("logged after 2 seconds");
3 }, 2000);
<ReplitEmbed src=”https://replit.com/@newlineauthors/callbacks-
example3” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/callbacks-
example4” />
The above code example shows that the setTimeout callback or the
callback provided to the setInterval function may not be invoked after the
specified time has passed; some other code can block them from being
invoked after the specified time.
Looking at the code example above, it is not hard to imagine that it will get
harder to read as more operations are added to the sequence of
asynchronous operations. Not only is it hard to read, but it is also hard to
maintain and refactor. Note that the code example above does not include
any error handling; add that to the code above, and you will have the
following:
1 asyncOperation1((error1, result1) => {
2 if (error1) {
3 // handle error
4 } else {
5 asyncOperation2(result1, (error2, result2) => {
6 if (error2) {
7 // handle error
8 } else {
9 asyncOperation3(result2, (error3, result3) => {
10 if (error3) {
11 // handle error
12 } else {
13 asyncOperation4(result3, (error4, result4) => {
14 if (error4) {
15 // handle error
16 } else {
17 // ...more nested asynchronous operations and callbacks
18 }
19 });
20 }
21 });
22 }
23 });
24 }
25 });
The code above fits its name: “Callback Hell”. No one wants to deal with
such a code. It is hard to reason about. We will see in later lessons in this
module how the same code can be rewritten using promises and async-
await syntax to make it more readable and maintainable.
Error handling
So, in short, the code we write that executes on the main thread only
initiates the asynchronous operation. Instead of waiting for the operation to
complete, the main thread is free to do other things. The asynchronous
operation is handled in the background by the environment in which our
JavaScript code is executed. It can be a browser or a runtime like NodeJS.
But how does the execution get back from the background to the main
thread where our code is executed? This is where the event loop comes into
the picture.
Let us understand the event loop using the following code example:
1 setTimeout(() => {
2 console.log("hello world");
3 }, 2000);
4
5 console.log("after setTimeout");
6
7 // output:
8 // ------
9 // after setTimeout
10 // hello world
<ReplitEmbed src=”https://replit.com/@newlineauthors/event-loop-
example1” />
The code above logs “after setTimeout” before “hello world” which is
inside the callback of setTimeout. The following steps explain how the
code above executes:
1. To execute the code, a task is created and pushed onto the call stack.
This is what’s commonly referred to as the “global execution context”.
2. Once the code execution starts, the first thing to do is to invoke the
setTimeout function, passing in a callback that is to be invoked after
approximately 2 seconds. Calling setTimeout starts a timer in the
background that will expire after 2 seconds in our code example. In the
meantime, the main thread continues executing the code instead of
waiting for the timer to expire. This is why “after setTimeout” is
logged before “hello world”.
3. Next, the console.log is executed, logging “after setTimeout” on the
console.
4. At this point, the synchronous execution of our code has ended. As a
result, the task created (step 1) to execute the code is popped off the
call stack. Now, JavaScript is ready to execute any scheduled
callbacks. This point is important: no asynchronous callback can be
invoked until the synchronous execution of the code has ended.
Remember, only one thing executes at a time on the main thread, and
the currently executing code cannot be interrupted.
5. After the synchronous execution ends, let us assume that by this time
the timer has expired (in reality, our code execution will end long
before 2 seconds). As soon as the timer expires, a task is enqueued in a
task queue to execute the callback of setTimeout. The task queue is
where different tasks are queued until they can be pushed onto the call
stack and executed.
6. The event loop is the entity that processes the tasks in the task queue
and pushes each of them to the call stack to execute them. Tasks are
processed in the order they are enqueued in the task queue. In our case,
there is only one task in the task queue. This task is pushed onto the
call stack, but the event loop only pushes the tasks onto the call stack if
the call stack is empty. In our case, the call stack is empty, so the
callback of setTimeout can be executed. As a result, “hello world” is
logged on the console.
The role of the event loop, as described in the above steps, is to process the
tasks in the task queue if the call stack is empty and there are one or more
tasks in the task queue waiting to be executed. So, the event loop is an
entity that allows asynchronous code to be executed in JavaScript in a non-
blocking manner. The event loop can be thought of as a loop that
continuously checks if there are any tasks waiting to be executed.
The event loop is what connects the two worlds: the “JavaScript world”,
where our code executes, and the “background world”, where the
asynchronous operations are actually executed.
Take your time to understand exactly what happens behind the scenes. The
timer is intentionally shown to take longer than 2 seconds to make the
visualization easier to understand. Understanding the steps above before
seeing the image will make it easy to understand how our code example
executes.
Any user interaction like the click event requires scheduling a task; the
same is true for executing the callbacks of timing functions like
setTimeout. Tasks are queued in the task queue until the event loop
processes them. The task queue is also referred to as the event queue or the
callback queue.
The event loop processes a single task during its single turn, commonly
referred to as the “event loop tick” or just “tick”. The next task is processed
during the next turn or tick of the event loop. The browser may choose to
render UI updates between tasks.
The event loop can have multiple sources of tasks, and the browser decides
which source to process tasks from during each tick of the event loop.
Another queue is known as the microtask queue, which we will discuss later
in this module. The event loop also processes microtasks, but there is a
difference in how the event loop processes tasks and microtasks. The
difference will be clear when we discuss the microtask queue.
In this lesson, we discussed what an event loop is and how tasks are
processed: a single task per tick of the event loop.
loupe
Further reading
Before we learn how we can create promise objects, let us first learn how
we can deal with promises using the built-in fetch function that allows us
to make HTTP requests from the JavaScript code running in the browser.
When the fetch function is called, instead of making the calling code wait
for the HTTP request to complete, it returns a promise object. We can
associate callback functions with the returned promise object to execute
code when the HTTP request is complete. We still use callbacks with
promises, but the problems we discussed with callbacks in an earlier lesson
in this module don’t exist when using promises. Compared to callbacks,
promises provide a clean and structured way to handle asynchronous
operations in JavaScript.
1 const p1 = fetch(/* some url */);
The promise returned by the fetch function can be thought of as the fetch
function promising us to supply a value when the HTTP request completes
some time in the future. In the meantime, the main thread is free to do other
things.
What can we do with the returned promise? We can register callbacks with
the promise object that will be invoked when the network request
completes. We can register separate callbacks to handle the success or
failure of the network request.
Promise states
pending: the initial state in which promises typically start when they
are created. It indicates that the asynchronous operation associated
with the promise is in progress.
fulfilled: means that the asynchronous operation associated with the
promise has been completed successfully.
rejected: means that the asynchronous operation associated with the
promise has failed.
During the lifecycle of a promise, its state changes from pending to either
fulfilled or rejected. The state of a promise is saved in the hidden internal
slot named [[PromiseState]].
Promise.prototype.then()
Promise.prototype.catch()
Promise.prototype.finally()
then method
catch method
We learned in the previous section that we can pass the rejection handler as
the second argument to the then method to handle the promise rejection.
There is another option to register the rejection handler, and that is through
the catch method. Instead of passing the rejection handler to the then
method, we can call the catch method on the promise to register the
rejection handler.
1 const p1 = fetch(/* some url */);
2
3 p1.then((response) => {
4 // code to execute if the promise fulfills
5 });
6
7 p1.catch((error) => {
8 // code to execute if the promise is rejected
9 });
The catch method is similar to the then method that is called with only a
rejection handler, as shown below:
1 p1.then(null, (error) => {
2 // code to execute if the promise is rejected
3 });
However, using the catch method to register a rejection handler is more
common than using the second argument of the then method.
finally method
We want to avoid code duplication, and the finally method can help us
remove the code duplication. The finally method allows us to execute
code regardless of promise fulfillment or rejection. Just like the then and
catch methods, the finally method also accepts a callback function that is
invoked asynchronously after promise fulfillment as well as promise
rejection. The callback passed to the finally method is the perfect place
for the code that we want to execute regardless of whether the
asynchronous operation fails or completes successfully. We can refactor the
code above as shown below:
1 const p1 = fetch(/* some url */);
2
3 p1.then((response) => {
4 // code to execute if the promise fulfills
5 });
6
7 p1.catch((error) => {
8 // code to execute if the promise is rejected
9 });
10
11 p1.finally(() => {
12 // hide the loading spinner
13 document.getElementById("spinner").style.display = "none";
14 });
Unlike the callbacks of the then and catch methods, the callback passed to
the finally method receives no arguments.
Creating promises
We can create new promise objects using the Promise constructor, as shown
below:
1 const p = new Promise((resolve, reject) => {
2 // initiate asynchronous operation...
3 });
<ReplitEmbed src=”https://replit.com/@newlineauthors/promises-
example9” />
The code example above shows how a promise object can be wrapped
around an asynchronous operation and be resolved or rejected when the
asynchronous operation succeeds or fails. In response to the promise being
fulfilled or rejected, the appropriate handler (fulfillment or rejection) is
invoked asynchronously.
The code example above might not make you see how promises are an
improvement over the traditional way of using callbacks to handle
asynchronous code, but wait until we discuss promise chaining and async-
await syntax in the upcoming lessons in this module. These two topics will
help you see how promises address the two main problems with callbacks:
Callback hell and error handling.
If you noticed in the code example in the previous section, we just wrapped
a promise around the callback-based XMLHttpRequest API. We wrapped a
promise around it so that we could interact with it using promises.
Similar to what we did above, we can convert any callback-based API into
a promise-based API. All we need to do is place the callback-based code in
the executor function and call the resolve and reject functions at
appropriate places to fulfill or reject the promise. Following is an example
of wrapping a promise around setTimeout to add an artificial delay in the
code:
1 function timeout(delayInSeconds) {
2 const delayInMilliseconds = delayInSeconds * 1000;
3
4 return new Promise((resolve) => {
5 setTimeout(() => resolve(), delayInMilliseconds);
6 });
7 }
8
9 timeout(2).then(() => {
10 console.log("done"); // logged after 2 seconds
11 });
<ReplitEmbed src=”https://replit.com/@newlineauthors/promises-
example10” />
In the code example above, we wrapped setTimeout in a promise to add an
artificial delay in the code. To resolve the promise after the specified delay
(2 seconds in our code above), we call the resolve function inside the
callback function of setTimeout. Note that we didn’t call or use the reject
function because we didn’t need it to reject the promise. We just want the
promise to be fulfilled after the specified delay.
Promise specification
Promise vs thenable
If you read the promise specification, you will find the word “thenable”
mentioned multiple times. A thenable is any object that has defined a
method named then but is not a promise. It is a generic term for objects
with a method named then. As promises have a method named then, we
can say that all promises are thenables, but the reverse is not true: every
thenable is not a promise.
Promise.prototype.then()
Promise.prototype.catch()
Promise.prototype.finally()
The following code example shows how we have registered fulfillment and
rejection handlers with the promise returned by the fetch function:
1 const p = fetch(/* some url */);
2
3 // register a fulfillment handler
4 p.then((response) => {
5 // code...
6 });
7
8 // register a rejection handler
9 p.catch((error) => {
10 // code...
11 });
As each promise instance method returns a new promise, we can rewrite the
above code as shown below:
1 fetch(/* some url */)
2 .then((response) => {
3 // code...
4 })
5 .catch((error) => {
6 // code...
7 });
The refactored code achieves the same result as the first code example, but
technically, the first code example is different compared to the refactored
code. In the first code example, the rejection handler is registered on the
promise returned by the fetch function, whereas in the refactored code, the
rejection handler is registered on the promise returned by the then method.
The promise chain in the refactored code can be split into different parts, as
shown below to make it easier to understand:
1 const pFetch = fetch(/* some url */);
2
3 const pThen = pFetch.then((response) => {
4 // code...
5 });
6
7 const pCatch = pThen.catch((error) => {
8 // code...
9 });
Notice the promises for which fulfillment and rejection handlers have been
registered. The fulfillment handler is registered on the promise returned by
the fetch function, but unlike the first code example, the rejection handler
is registered on the promise returned by the then method. So does it mean
that if the promise returned by the fetch function is rejected, there is no
rejection handler registered to handle the rejection? No, the rejection
handler registered on the pThen promise will handle the rejection. To
understand how it works, we have to understand how the promise chain
works.
Further code examples in this lesson will use the following function that
simulates an HTTP request that takes approximately two seconds to
complete:
1 function fakeRequest(isSuccessRequest = true) {
2 return new Promise((resolve, reject) => {
3 setTimeout(() => {
4 if (isSuccessRequest) {
5 const data = { name: "John Doe", favouriteLanguage: "JavaScript" };
6 resolve(data);
7 } else {
8 const error = new Error("request failed");
9 reject(error);
10 }
11 }, 2000);
12 });
13 }
This above function will make it easy for us to understand the promise
chaining. The function takes a boolean parameter that specifies whether we
want our fake request to be fulfilled or rejected. The default value of the
parameter is true, so we only need to pass the argument if we want the
request to fail. Inside the function, a promise is returned that wraps around
the setTimeout to simulate a request that takes approximately two seconds
to complete. After the timer expires, the promise is fulfilled or rejected,
depending on the value of the isSuccessRequest parameter.
With the fakeRequest function defined, let us dive into the world of
promise chaining.
then promise
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example5” />
The promise returned by the then method fulfills or gets rejected based on
the following two questions:
Keeping the above two questions in mind, let us discuss the different
scenarios that can affect the promise returned by the then method:
If the original promise on which the then method is called is fulfilled, the
promise returned by the then method depends on what happens inside the
fulfillment handler. Following are different things a fulfillment handler can
do that affect the promise returned by the then method:
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example6” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example7” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example8” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example9” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example10” />
In the above code example, pThen promise returned by the then method
gets resolved to the promise returned by the fulfillment handler of the
pRequest promise.
If the original promise on which the then method is called is rejected, the
promise returned by the then method depends on the following scenarios:
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example11” />
Now that we have discussed how the promise returned by the then method
settles in different scenarios, next we will discuss the promise returned by
the catch method.
catch promise
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example12” />
The rejection handler registered using the catch handler is called when the
original promise on which the catch method is called gets rejected. If the
original promise is rejected, then the promise returned by the catch method,
just like the promise returned by the then method, depends on what
happens inside the rejection handler:
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example13” />
If the rejection handler doesn’t explicitly return any value, the promise
returned by the catch method is fulfilled with undefined as the
fulfillment value.
1 // pRequest will get rejected
2 const pRequest = fakeRequest(false);
3
4 const pCatch = pRequest.catch((error) => {
5 console.log(error.message); // request failed
6 });
7
8 pCatch.then((data) => {
9 console.log(data); // undefined
10 });
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example14” />
If the catch method is called on the original promise but the rejection
handler isn’t provided, the promise returned by the catch method gets
rejected with the same rejection value as the original promise.
1 // pRequest will get rejected
2 const pRequest = fakeRequest(false);
3
4 // rejection handler not registered
5 const pCatch = pRequest.catch();
6
7 pCatch.catch((error) => {
8 // logs the rejection value of the
9 // original pRequest promise
10 console.log(error.message); // request failed
11 });
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example15” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example16” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example17” />
finally promise
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example18” />
Note that the finally callback didn’t explicitly return any value, but the
finally promise fulfilled with the fulfillment value of the original
pRequest promise. This behavior is different than that of the then and
catch methods; their promise is fulfilled with the value undefined if their
callback implicitly returns undefined.
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example19” />
Scenario 3: shadowing original promise settlement
Unlike the then and catch methods, the return value of the finally
callback is ignored, and the promise returned by the finally method
simply meets the same fate as the original promise on which it is called; if
the original promise fulfills, the finally promise also fulfills; if the original
promise gets rejected, the finally promise also gets rejected.
However, the two conditions mentioned in the first two scenarios of the
finally method are exceptions to this rule. If the finally callback throws
an error, the finally promise gets rejected with the thrown value.
1 // pRequest will get rejected
2 const pRequest = fakeRequest(false);
3
4 const pFinally = pRequest.finally(() => {
5 throw new Error("finally error");
6 });
7
8 pFinally.catch((error) => {
9 console.log(error.message); // finally error
10 });
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example20” />
The rejection of the finally method shadowed the rejection of the original
pRequest promise. The same thing will happen if the pRequest promise is
fulfilled. Throwing an error from the finally callback simply rejects the
finally promise, regardless of what happens to the original promise.
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example21” />
We have discussed every scenario that can reject or fulfill the promise
returned by each of the promise instance methods. Finally, we can make
sense of how the promise chain works. We will go through a series of
examples to solidify our understanding.
Example 1
1 fakeRequest()
2 .then((response) => {
3 console.log(response);
4 return "hello world";
5 })
6 .then((data) => {
7 console.log(data);
8 return "123";
9 })
10 .catch((error) => {
11 console.log(error.message);
12 });
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example22” />
What output do you expect in the above code example? Keep in mind that
each instance method returns a new promise, and its fulfillment or rejection
depends on two things:
What happens to the original promise on which the method is called?
What happens inside the callback function of that method?
Considering the different scenarios discussed above that can fulfill or reject
the promise returned by each of the promise instance methods, try to test
your understanding by guessing the output of the above code. Below is an
explanation of the output produced by the above code:
1. Starting from the top of the promise chain, the promise returned by the
fakeRequest function will be fulfilled, resulting in the invocation of
its fulfillment handler that is registered using the first invocation of the
then method. As a result, the following is logged on the console:
1 {
2 favouriteLanguage: "JavaScript",
3 name: "John Doe"
4 }
3. At this point, two promises in the promise chain have settled. The next
promise in the chain is the one returned by the second then method.
Just like the first then method, the promise returned by the second
then method depends on the original promise on which it is called, i.e.,
the promise returned by the first then method. As the original promise
is fulfilled, the second then promise now depends on what happens
inside its callback function. Its callback returns the string “123”. So the
second then promise fulfills with “123” as its fulfillment value.
But there is no fulfillment handler registered for the second then
promise; only the rejection handler is registered using the catch
method. As a result, no fulfillment handler is invoked in response to
the fulfillment of the second then method. The promise chain moves
to the last promise in the chain, i.e., the one returned by the catch
method.
Example 2
1 fakeRequest()
2 .then((response) => {
3 console.log(response);
4 return fakeRequest();
5 })
6 .then((data) => {
7 console.log(data);
8 })
9 .catch((error) => {
10 console.log(error.message);
11 });
1. Starting from the top of the promise chain, the promise returned by the
fakeRequest function will be fulfilled, resulting in the invocation of
its fulfillment handler that is registered using the first invocation of the
then method. As a result, the following is logged on the console:
1 {
2 favouriteLanguage: "JavaScript",
3 name: "John Doe"
4 }
Example 3
1 fakeRequest(false)
2 .then((response) => {
3 console.log(response);
4 })
5 .catch((error) => {
6 console.log(error.message);
7 });
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example24” />
1. Starting from the top of the promise chain, the promise returned by the
fakeRequest function will get rejected, but there is no rejection
handler registered for this promise; only a fulfillment handler is
registered. As a result, no rejection handler will be invoked for this
promise. We move on to the next promise in the chain.
2. The promise returned by the fakeRequest function has been settled.
The next promise in the chain is the one returned by the first then
method call. As the original promise is rejected and no rejection
handler was passed to the then method, the promise returned by the
then method will also get rejected with the same rejection value as the
original promise. As a result, its rejection handler, registered using the
catch method, is invoked, logging the following on the console:
1 "request failed"
The error object with which the first promise got rejected is the same
value with which the promise returned by the then method also got
rejected. The promise chain moves to the last promise in the chain, i.e.,
the one returned by the catch method.
3. At this point, two promises in the promise chain have settled. The next
promise in the chain is the one returned by the catch method. As the
then promise is rejected, the promise returned by the catch method
depends on what happens inside its callback. Its callback implicitly
returns undefined, resulting in the catch promise getting fulfilled with
undefined as a fulfillment value. But there is no fulfillment handler
registered for the catch promise, so its fulfillment is simply ignored.
The final output of the code is shown below:
1 "request failed"
One thing to note in the above code example is that the rejection of the
promise returned by the fakeRequest function was eventually handled by
the rejection handler registered for the promise returned by the then
method. This is one of the powers of promise chaining. Unlike callbacks,
where we had to check for the error in every callback, with promise
chaining, we can register one rejection handler, and it can handle the
rejection of all the promises that come before it in the promise chain. We
could have multiple then method calls in the promise chain and only one
rejection handler at the end of the promise chain, registered using the catch
method. This makes error handling easy to manage in a promise chain.
Example 4
1 fakeRequest()
2 .then((response) => {
3 return fakeRequest(false);
4 })
5 .catch((error) => {
6 return { data: "default data" };
7 })
8 .then((data) => {
9 console.log(data);
10 })
11 .then(() => {
12 throw new Error("error occurred");
13 })
14 .catch((error) => {
15 console.log(error.message);
16 });
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-chaining-
example25” />
1. Starting from the top of the promise chain, the promise returned by the
fakeRequest function will be fulfilled, resulting in the invocation of
its fulfillment handler that is registered using the first invocation of the
then method, passing the fulfillment value to its fulfillment handler as
an argument.
2. The promise returned by the fakeRequest function has been settled.
The next promise in the chain is the one returned by the first then
method call. As the original promise is fulfilled, the first then promise
now depends on what happens inside its callback function. As it is
returning a new promise by calling the fakeRequest function, the first
then promise will get resolved to the promise returned from its
callback function. The then promise will wait for the promise returned
from its callback function to settle before settling itself.
The promise returned from the callback function of the first then
method will be rejected after approximately two seconds. As soon as it
is rejected, the promise returned by the then method will also get
rejected with the same rejection value as the promise returned by its
callback. As a result, its rejection handler is called, which was
registered using the first catch method call. The rejection handler of
the first then promise receives its rejection value as an argument.
3. At this point, the first two promises in the promise chain have settled.
The next promise in the chain is the one returned by the first catch
method. As the promise on which the catch method is called gets
rejected, the first catch promise now depends on what happens inside
its callback function. It returns an object literal. As a result, the first
catch promise fulfills the returned object as its fulfillment value.
Note that the catch method doesn’t necessarily have to be at the end of
the promise chain; however, the catch method is most commonly
placed at the end of the promise chain. Depending on the requirement,
the catch method can be placed anywhere in the chain. In our code
example, it is called after the first then method to handle the possible
rejection of the first then promise by returning the default data and
letting the chain continue. If the rejection of the first then promise
wasn’t handled, all the then promises after the first then method
would also be rejected with the same rejection value as the first then
promise, and the rejection would finally be handled in the last catch
method call.
4. At this point, the first three promises in the promise chain have settled.
The next promise in the chain is the one returned by the second then
method call. As the promise (the first catch promise) on which the
second then method is called is fulfilled, the promise returned by the
second then method now depends on what happens inside its callback
function. Its callback logs the fulfillment value of the first catch
promise and implicitly returns undefined, resulting in the second then
promise getting fulfilled with undefined as the fulfillment value.
Following is the console output up to this point:
1 {
2 data: "default data"
3 }
5. At this point, the first four promises in the promise chain have settled.
The next promise in the chain is the one returned by the third then
method call. As the promise (the second then promise) on which the
third then method is called is fulfilled, the promise returned by the
third then method now depends on what happens inside its callback
function. Its callback throws an error, resulting in the third then
promise getting rejected with the thrown error as the rejection reason
or value. As a result, its rejection handler, registered using the last
catch method, is invoked, passing in the rejection value as an
argument.
6. As the promise (the third then promise) on which the last catch
method is called gets rejected, the last catch promise depends on what
happens inside its callback function. Its callback logs the rejection
value of the third then promise and implicitly returns undefined,
resulting in the last catch promise getting fulfilled with undefined as
the fulfillment value. But there is no fulfillment handler registered for
the last catch promise, so its fulfillment is simply ignored. The final
console output of the code is shown below:
1 {
2 data: "default data"
3 }
4
5 "error occurred"
Hopefully, the examples above, along with the earlier discussion in this
lesson on different scenarios that can reject or fulfill the promise returned
by each of the promise instance methods, have laid a solid foundation for
understanding the promise chains and making use of them in your own
code.
There is one thing that should be kept in mind when registering a rejection
handler using the then method: the rejection handler registered using the
then method is not invoked if the promise returned by the then method, to
which the rejection handler is passed as an argument, gets rejected. The
following code example shows this in action:
1 fakeRequest().then(
2 (response) => {
3 // rejects the promise returned
4 // by the "then" method
5 throw new Error("error");
6 },
7 (error) => {
8 // this callback is not invoked
9 console.log(error.message);
10 }
11 );
The rejection handler registered using the then method is only invoked if
the original promise on which the then method is called gets rejected. As a
result, we have an unhandled promise rejection in the above code example,
which, in the worst case, can terminate the program. As a result, always
remember to handle all the possible promise rejections in your code when
working with promises.
In this lesson, we will discuss a couple of common use cases of the two
static methods of promises and learn how they can be useful. Other promise
static methods are also useful, but in my opinion, the following two use
cases are the most common ones:
Making concurrent requests
Implementing request timeout
Concurrent requests
<ReplitEmbed src=”https://replit.com/@newlineauthors/static-promise-
methods-example1” />
As each request in the above code is independent, instead of initiating the
requests in a sequential manner, we want concurrent requests. This can be
achieved using the Promise.all method. It allows us to start each request
one after the other without waiting for one request to complete before
starting the other one. As a result, all three requests are initiated
concurrently, and we can wait for their collective results.
<ReplitEmbed src=”https://replit.com/@newlineauthors/static-promise-
methods-example2” />
Request timeout
An HTTP request can sometimes just hang due to some problem on the
server. We do not want the request to be in a pending state for longer than a
few seconds. To avoid longer request pending times, we can implement the
request timeout feature, which lets our code know that a request is taking
longer than expected. This allows us to take the appropriate action.
The following code example shows the request timeout implemented using
the Promise.race method:
1 // simulate a request that takes
2 // approximately 8 seconds to complete
3 function delayedRequest() {
4 return new Promise((resolve, reject) => {
5 setTimeout(() => {
6 resolve("hello world");
7 }, 8000);
8 });
9 }
10
11 // timeout promise that is rejected
12 // after approximately 3 seconds
13 function timeout() {
14 return new Promise((resolve, reject) => {
15 setTimeout(() => {
16 const error = new Error("request timed out");
17 reject(error);
18 }, 3000);
19 });
20 }
21
22 Promise.race([delayedRequest(), timeout()])
23 .then((response) => {
24 console.log(response);
25 })
26 .catch((error) => console.log(error.message));
<ReplitEmbed src=”https://replit.com/@newlineauthors/static-promise-
methods-example3” />
In this lesson, we discussed only two static methods, but it’s worth learning
about the other static methods available on the Promise constructor. Each
static method has its own use cases; we just discussed two use cases that I
think are the most commonly needed.
The async await can be considered a syntax sugar over the traditional way
of using promises. It allows us to deal with promises using code that
executes asynchronously but looks synchronous. It also allows us to write
more concise code that is easier to reason about, as the code doesn’t include
callbacks, and the flow of the code looks like that of synchronous code.
Let’s take a look at the following code example that uses promise chaining:
1 function fetchTodo(url) {
2 fetch(url)
3 .then((response) => {
4 if (response.ok) {
5 return response.json();
6 } else {
7 throw new Error("request failed");
8 }
9 })
10 .then((data) => {
11 console.log(data);
12 })
13 .catch((error) => {
14 console.log(error.message);
15 });
16 }
17
18 const url = "https://jsonplaceholder.typicode.com/todos/1";
19 fetchTodo(url);
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example2” />
The revised code achieves the same result but is more readable, doesn’t use
any callbacks, and is easier to reason about as compared to the earlier
example that uses promise chaining. Although the code looks like
synchronous code, it is asynchronous. Let us understand how async await
works.
Two things should be noted in the code example above that uses the async
await syntax: the async keyword in the function signature and the await
keyword inside the function. Following are the two main steps to using the
async await syntax:
1. Mark any function as “async” using the async keyword. This is needed
because the await keyword can only be used inside an “async”
function.
2. Use the await keyword inside the async function to wait for any
promises to settle.
:::info While the await keyword is mostly used inside an async function
because the await keyword was only allowed inside async functions until a
recent change in the language that allows using the await keyword at the
top-level of a module
:::
async functions
An async function allows the use of the await keyword inside its body. An
async function is different from a non-async function because an async
function always returns a promise. An async function implicitly creates and
returns a promise, similar to how each promise instance method creates and
returns a new promise. The following code verifies this claim:
1 async function foo() {}
2
3 const result = foo();
4 console.log(result instanceof Promise); // true
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example3” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example4” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example5” />
Throwing an error inside the async function rejects the async function
promise, using the thrown value as the rejection reason.
1 async function foo() {
2 throw new Error("some error occurred");
3 }
4
5 foo().catch((error) => console.log(error.message)); // some error occurred
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example6” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example7” />
await keyword
The await keyword, also referred to as the await operator, is used to wait
for a promise to settle. The following is an example of using the await
keyword to wait for a promise to settle:
1 // assume that the following statement
2 // is inside an async function
3 const response = await fetch(url);
What’s important to note is that the code inside the async function is
executed synchronously until the first await expression. What if the async
function doesn’t have the await keyword inside it? Will the function
execute synchronously? Yes, it will, but keep in mind that the async
function always returns a promise, and it will either get fulfilled or rejected
depending on what happens inside the async function. This means that the
following code doesn’t work as one might expect:
1 async function foo() {
2 return "123";
3 }
4
5 const result = foo();
The async function in the above code example didn’t use the await
keyword, so the code inside it is executed synchronously, but does it return
the value “123” synchronously as well? No, it doesn’t. The function is
async, which means it returns a promise, so the result in the above
example contains the promise and not the value “123”. To get the
fulfillment value of the promise, we can either use promise chaining as
shown below:
1 async function foo() {
2 return "123";
3 }
4
5 foo().then(console.log); // "123"
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example10” />
Or await the promise returned by the foo function using the async await
syntax as shown below:
1 async function foo() {
2 return "123";
3 }
4
5 async function bar() {
6 const result = await foo();
7 console.log(result); // "123"
8 }
9
10 bar();
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example11” />
An async function is not limited to using the await keyword only once
inside its body. You can use the await keyword as many times as you want
inside an async function. The only thing to be aware of regarding multiple
await expressions is that they are not executed in parallel; instead, they are
executed in sequence, one after the other. The function execution will be
paused at each await expression, and the next await expression can only be
executed after the ones before it has been executed.
1 // returns a promise that is fulfilled
2 // after approximately 1 second
3 function promisifiedRandomNumber() {
4 return new Promise((resolve, reject) => {
5 setTimeout(() => {
6 // generate a random number within range: 0 - 9
7 const randomNum = Math.floor(Math.random() * 10);
8 resolve(randomNum);
9 }, 1000);
10 });
11 }
12
13 async function random() {
14 const num1 = await promisifiedRandomNumber();
15 const num2 = await promisifiedRandomNumber();
16 const num3 = await promisifiedRandomNumber();
17
18 console.log(num1, num2, num3);
19 }
20
21 random();
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example12” />
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example13” />
The two await expressions in the above code example are also not executed
in parallel; instead, they are executed one after the other, from left to right.
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example14” />
Error handling
If any of the promises awaited in the try block are rejected, the code after
that await expression won’t be executed, and the execution will jump to the
catch block. The await keyword throws the promise rejection value,
allowing the catch block to catch the rejection.
Alternatively, we can omit the try-catch block, but in this case, the code
that calls the async function must handle the promise rejection, either by
using the promise chaining:
1 async function getUsersAndTasks() {
2 const users = await fetchUsers();
3 const tasks = await fetchTasks();
4
5 // do something with users and tasks.
6 }
7
8 getUsersAndTasks().catch((error) => {
9 /* handle the error */
10 });
or using the try-catch block in the calling code if we are using the async
await syntax:
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example18” />
The getPromise function returns a promise that can be rejected, but the
promise returned by the foo function is always fulfilled. Why? The foo
function has a problem: it didn’t return or await the promise returned by the
getPromise function. As a result, the foo function doesn’t wait for the
promise returned by the getPromise function to settle; instead, it just calls
the getPromise function, and the function execution ends, implicitly
returning undefined, leading to the foo function promise getting fulfilled
with undefined as the fulfillment value.
Further code examples will use the getPromise function defined above.
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example19” />
Returning the promise resolves the foo function of the promise returned
inside its body. As a result, whatever happens to the promise returned by
getPromise, the foo function promise meets the same fate.
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example20” />
However, one thing to note in this code example is that if the promise
returned by getPromise is fulfilled, the foo function promise doesn’t fulfill
with its fulfillment value; instead, it fulfills with undefined as the
fulfillment value because we didn’t explicitly return anything from the foo
function, and we know what happens to the async function promise when
we don’t explicitly return any value inside the function: the async function
promise gets fulfilled with undefined as the fulfillment value.
await the promise returned by the getPromise function and surround it
with the try-catch block.
1 async function foo() {
2 try {
3 await getPromise();
4 } catch (error) {
5 console.log("inside catch block of foo function");
6 return "error caught in foo";
7 }
8 }
9
10 foo()
11 .then(() => console.log("foo promise fulfilled"))
12 .catch(() => console.log("foo promise failed"));
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example21” />
Awaiting the getPromise call will catch the promise rejection, causing the
catch block to execute. However, the promise returned by the foo function
will always be fulfilled. Why? Because the catch block didn’t throw an
error or return a promise that gets rejected. As a result, the foo function
promise always fulfills with the return value of the catch block. We can
throw the error from the catch to fix this problem. Having said that, if all
we do inside the catch block is throw the error, then it’s better to just omit
the try-catch block and let the promise rejection automatically reject the
foo function promise.
Another problem in this code example is that we didn’t explicitly return any
value in case the promise was fulfilled and the catch block was never
executed. So the foo function promise will be fulfilled with undefined.
Adding the return keyword before the await expression will do the job.
The await keyword is usually used to wait for a promise to settle, but it can
also be used with a non-promise value. The following code example shows
this action:
1 const printRandomNumber = async () => {
2 const randomNum = await Math.floor(Math.random() * 10);
3 console.log(randomNum);
4 };
5
6 printRandomNumber();
7
8 console.log("before printing random number");
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-await-
example22” />
If you execute the above code example, you will note that the console.log
statement at the end of the code example is logged before the random
number is printed, even though the function is called before the last
console.log statement. Why is that? We haven’t awaited any promises, so
what’s happening here?
When the await keyword is used with a non-promise value, a new promise
is created, and that promise is fulfilled with the value we used with the
await keyword. In our code example, we have awaited a random number; it
is not a promise, so a new promise is created and fulfilled with the
generated random number. Code after the await expression is executed as
though it were in a fulfillment handler. As a result, when the promise is
fulfilled, the code after the await expression is not immediately executed. It
is executed asynchronously, and as we learned in the lesson about event
loop, any asynchronous code is only executed after the synchronous
execution of our code ends. The last console.log statement is executed as
part of the synchronous execution of our code. As a result, it is logged
before the random number.
Using await with a non-promise value is hardly useful, but just be aware
that it is possible, and the value is implicitly wrapped in a promise.
We learned in one of the earlier lessons in this module that executing DOM
event listeners and setTimeout or setInterval callbacks require
scheduling a “task”. Tasks are enqueued in a task queue until the event loop
processes them. What about the promise fulfillment or rejection handlers?
Does their execution also require scheduling a task? Not exactly a task, but
a “microtask”.
Whereas “tasks” are executed in the order they are enqueued in the task
queue, only one task is executed per one turn or tick of the event loop.
Another important thing to note about microtasks is that while only one task
is processed per tick of the event loop, microtasks are processed until the
microtask queue is empty. If a task schedules another task, it won’t be
processed until the next turn or tick of the event loop, but in the case of
microtasks, if any microtask is queued by a microtask, the queued
microtask will also be processed. This means that the event loop can get
stuck in an infinite loop if each microtask keeps queuing another microtask.
<ReplitEmbed src=”https://replit.com/@newlineauthors/microtasks-
example1” />
Executing the above code requires scheduling tasks and microtasks. The
following steps explain how different tasks and microtasks are scheduled to
execute the code above:
3. Next, we have the setTimeout call with a 500ms delay. This starts a
timer in the background, and its expiration will result in a task to
execute the setTimeout callback getting queued in the task queue.
1 task queue:
2 -----------
3 [task(execute setTimeout callback)]
4
5 output:
6 -------
7 start
5. Next, we have a setTimeout call with a 0ms delay. This also schedules
a task to execute its callback.
1 task queue:
2 -----------
3 [
4 task(execute setTimeout callback),
5 task(execute setTimeout callback)
6 ]
7
8 microtask queue:
9 ----------------
10 [job(execute fulfillment callback)]
11
12 output:
13 -------
14 start
6. Finally, the synchronous execution reaches its end with the final
console.log statement, logging “end” on the console. At this point,
the callstack is empty, and the event loop can start processing the
scheduled tasks and microtasks.
1 task queue:
2 -----------
3 [
4 task(execute setTimeout callback),
5 task(execute setTimeout callback)
6 ]
7
8 microtask queue:
9 ----------------
10 [job(execute fulfillment callback)]
11
12 output:
13 -------
14 start
15 end
10. Similar to step 8, the callback function of the second then method
implicitly returns undefined, and as a result, the promise returned by
the then method is fulfilled with undefined as the fulfillment value.
This queues another microtask in the microtask queue to execute the
fulfillment handler of the promise returned by the second then
method.
1 task queue:
2 -----------
3 [
4 task(execute setTimeout callback),
5 task(execute setTimeout callback)
6 ]
7
8 microtask queue:
9 ----------------
10 [job(execute fulfillment callback)]
11
12 output:
13 -------
14 start
15 end
16 first 'then' callback
17 second 'then' callback
11. This results in the “third ‘then’ callback” getting logged on the
console.
1 task queue:
2 -----------
3 [
4 task(execute setTimeout callback),
5 task(execute setTimeout callback)
6 ]
7
8 microtask queue:
9 ----------------
10 []
11
12 output:
13 -------
14 start
15 end
16 first 'then' callback
17 second 'then' callback
18 third 'then' callback
12. We didn’t do anything with the promise returned by the third then
method, so its fulfillment is ignored. At this point, all the microtasks
have been processed, and the microtask queue is empty. The event
loop can now process the first task in the task queue.
13. The first task in the task queue is that of the second setTimeout call
because it had less delay than the first one, so it was queued before the
task of the other setTimeout callback, which had a 500ms delay.
Processing it results in a “setTimeout callback with 0ms delay” being
logged on the console.
1 task queue:
2 -----------
3 [task(execute setTimeout callback)]
4
5 microtask queue:
6 ----------------
7 []
8
9 output:
10 -------
11 start
12 end
13 first 'then' callback
14 second 'then' callback
15 third 'then' callback
16 setTimeout callback with 0ms delay
14. Finally, the last task in the task queue is that of the first setTimeout
call with a 500ms delay, resulting in “setTimeout callback with 500ms
delay” getting logged on the console.
1 task queue:
2 -----------
3 []
4
5 microtask queue:
6 ----------------
7 []
8
9 output:
10 -------
11 start
12 end
13 first 'then' callback
14 second 'then' callback
15 third 'then' callback
16 setTimeout callback with 0ms delay
17 setTimeout callback with 500ms delay
Having said that, the term “resolved” can also be used to refer to a promise
that has either been fulfilled or rejected. For more details, read: promises-
unwrapping - States and Fates
:::
Further reading
The above code will work if you pass a URL to the fetchData function and
then wait for the promise to resolve, but the use of the Promise constructor
is unnecessary in the above code example. The fetch function already
returns a promise, so instead of wrapping the fetch function call with the
promise constructor, we can re-write the above function as shown below:
1 function fetchData(url) {
2 return fetch(url).then((res) => res.json(res));
3 }
The revised version of the fetchData function is concise, easy to read, free
from the creation of any unnecessary promises, and allows the code that
calls the fetchData function to catch and handle any errors. The older
version of the fetchData function also allowed the calling code to handle
errors, but the revised version does it without using the catch method call.
When writing code that uses promises, one of the most important rules to
keep in mind is to either catch and handle the error or return the promise to
allow the calling code to catch and handle it. This fundamental rule can
help you avoid hidden bugs in the code that uses promises.
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-anti-
patterns-example3” />
The above code throws an error because the fetchData function doesn’t
return the promise. It also doesn’t allow the calling code to do any kind of
error handling.
Return the promise from the fetchData function by adding the return
keyword before fetch(...).
1 function fetchData(url) {
2 return fetch(url).then((response) => response.json());
3 }
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-anti-
patterns-example4” />
As the above function just makes the HTTP request and returns the
response data after calling the json() method on the response object,
the calling code is responsible for using the response data as well as
handling any error.
1 fetchData(/* some url */)
2 .then((data) => {
3 /* do something with the data */
4 })
5 .catch((error) => {
6 /* handle error */
7 });
Handle the error inside the fetchData function by chaining the catch
method to the then method.
1 function fetchData(url) {
2 fetch(url)
3 .then((response) => response.json())
4 .then((data) => {
5 /* do something with the data */
6 })
7 .catch((err) => {
8 /* error handling code */
9 });
10 }
and you call the function above as shown below:
1 fetchData(/* some url */);
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-anti-
patterns-example8” />
See this stackoverflow post, which explains this behavior in more detail.
:::info
:::
Suppose you are wondering why the promise returned by the catch method
got fulfilled instead of getting rejected. In that case, the answer is that, as
explained in the previous lesson, the promise returned by the then or catch
method gets fulfilled if their callback function explicitly or implicitly
returns a value instead of throwing an error or returning a rejected promise
or a promise that eventually gets rejected.
So, how can we fix the above code example to avoid this problem? There
are two ways to fix this problem:
Throw the error from the callback function of the catch method.
1 function getData(url) {
2 return Promise.reject(new Error()).catch((err) => {
3 throw err;
4 });
5 }
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-anti-
patterns-example9” />
This will reject the promise returned by the catch method, and the getData
function will return this rejected promise. As a result, as expected, catch
method callback in the calling code will be invoked.
<ReplitEmbed src=”https://replit.com/@newlineauthors/promise-anti-
patterns-example10” />
This will also invoke the catch block in the calling code because now
the getData function returns the result of calling Promise.reject, and
as mentioned before, Promise.reject creates a rejected promise. :::tip
Personally, I recommend using this approach instead of throwing the
error from the catch method callback. Just allow the calling code to
catch and handle the errors. The catch method callback that just re-
throws the error is unnecessary. :::
Suppose the executor function is an async function. In that case, any errors
thrown by the async executor function will not be caught, and the thrown
error won’t cause the newly-constructed promise to reject.
1 const p = new Promise(async (resolve, reject) => {
2 throw new Error("error");
3 });
4
5 p.catch((e) => console.log(e.message));
Another thing to note is that if you find yourself using await inside the
executor function, this should be a signal to you that you don’t need the
promise constructor at all (remember the first anti-pattern discussed above).
Iterators and Generators
Built-in objects like arrays can be iterated over using the for…of loop.
Instead of iterating over an array with a simple for loop where we have to
access the value using an index, increment the index after each iteration,
and also know when to end the iteration so that we don’t access indexes that
are out of bounds, with for...of loop, everything is handled for us. We
don’t have to worry about the indexes or the loop termination condition.
1 const arr = [1, 2, 3];
2
3 for (const num of arr) {
4 console.log(num);
5 }
<ReplitEmbed src=”https://replit.com/@newlineauthors/iterators-
example1” />
How does for...of loop help us iterate over an array? How does it know
when to end the iteration?
Iterables
Iterators
Iterables
What does the Symbol.iterator return that can be used by constructs like
for...of loop to iterate over an object? It returns an iterator object.
Iterators
Iterators are objects that implement the iterator protocol. According to the
iterator protocol, an object is an iterator if it implements a method named
next that takes zero or one argument and returns an object with the
following properties:
The following are examples of iterator objects with the above mentioned
properties:
1 { value: 45, done: false }
2
3 // or
4
5 { value: undefined, done: true }
When we iterate over an array using the for...of loop, it internally gets
the iterator from the array and keeps calling its next method until the
iterator has returned all values. With the for...of loop, we use the iterator
indirectly. We can also use the iterator directly. Arrays are iterables, and we
know that iterables implement the Symbol.iterator method that returns
the iterator object that contains the next method. The following code
example shows how we can get the array iterator and use it directly to get
the values in the array:
1 const arr = [2, 4, 6, 8, 10];
2
3 // get the array iterator object
4 const arrayIterator = arr[Symbol.iterator]();
5
6 // get the first iterator result object
7 let result = arrayIterator.next();
8
9 // keep getting new iterator result objects
10 // until the "done" property of the iterator
11 // result object is false
12 while (!result.done) {
13 console.log(result.value);
14 result = arrayIterator.next();
15 }
16
17 /*
18 2
19 4
20 6
21 8
22 10
23 */
<ReplitEmbed src=”https://replit.com/@newlineauthors/iterators-
example2” />
Each built-in iterator object provides iterators that define a specific iteration
behavior for the iterable object. The following is an example of using the
Map object’s iterator directly:
<ReplitEmbed src=”https://replit.com/@newlineauthors/iterators-
example3” />
The value of the value property on the iterator result object for the Map
object is a key-value pair contained in an array. You can use
Map.prototype.values() to get an iterator that just returns values in the Map,
or Map.prototype.keys() to get an iterator that returns all the keys in the
Map.
Iterator prototype
Each iterator object inherits from the respective iterator prototype object.
For example, the array iterator inherits from the Array Iterator prototype
object. Similarly, the string iterator inherits from the String Iterator
prototype object. All iterator prototype objects inherit from the
Iterator.prototype object.
<ReplitEmbed src=”https://replit.com/@newlineauthors/iterators-
example4” />
The code example above shows one way to get the array iterator prototype
object, and it also shows that the next method is inherited from the array
iterator prototype object. If we get the prototype of the array iterator
prototype object, we will get the Iterator.prototype object, which is
shared by all iterator prototype objects.
1 const arr = [2, 4, 6, 8, 10];
2
3 // get the array iterator object
4 const arrayIterator = arr[Symbol.iterator]();
5
6 // this is the prototype object shared by all array iterators
7 const arrayIteratorPrototype = Object.getPrototypeOf(arrayIterator);
8
9 // this is the Iterator.prototype object shared by all iterator prototypes
10 const iteratorPrototype = Object.getPrototypeOf(arrayIteratorPrototype);
:::
<ReplitEmbed src=”https://replit.com/@newlineauthors/iterators-
example6” />
Making custom iterable objects
At this point, we have learned enough about iterables and iterators to make
custom iterable objects. To make a custom iterable object, we need to
implement Symbol.iterator method that returns the iterator object
containing the next method. Let’s consider an example of student objects
that we want to make iterable so that we can easily print their properties
with the for...of loop.
1 function Student(name, age, id, courses) {
2 this.name = name;
3 this.age = age;
4 this.id = id;
5 this.courses = courses;
6 }
This is the Student constructor that will be used to make student objects.
To make all the student objects iterable, we need to implement the
Symbol.iterator method in the Student.prototype object, as shown
below:
1 Student.prototype[Symbol.iterator] = function () {
2 // "this" refers to the student object on which this method is called
3 const currentStudent = this;
4 const studentProps = Object.getOwnPropertyNames(currentStudent);
5 let propIndex = 0;
6
7 const studentIterator = {
8 next: () => {
9 if (propIndex < studentProps.length) {
10 const key = studentProps[propIndex];
11 const value = currentStudent[key];
12 propIndex++;
13 const formattedValue = `${key.padStart(7)} => ${value}`;
14
15 return {
16 value: formattedValue,
17 done: false
18 };
19 }
20
21 return {
22 value: undefined,
23 done: true
24 };
25 }
26 };
27
28 return studentIterator;
29 };
Now, if we try to iterate over any student instance, we will get the formatted
values as we defined in the student iterator’s next method, as shown below:
1 const jack = new Student("Jack", 20, "21A", ["Maths", "Biology", "Physics"]);
2
3 for (const val of jack) {
4 console.log(val);
5 }
6
7 /*
8 name => Jack
9 age => 20
10 id => 21A
11 courses => Maths,Biology,Physics
12 */
<ReplitEmbed src=”https://replit.com/@newlineauthors/iterators-
example9” />
Remember that each iterator prototype object, for example, the array
iterator prototype object, inherits from the Iterator.prototype object, but
the studentIterator object doesn’t. As a result, the student iterator object
is not iterable.
1 const jack = new Student("Jack", 20, "21A", ["Maths", "Biology", "Physics"]);
2
3 const studentIterator = jack[Symbol.iterator]();
4
5 for (const val of studentIterator) {
6 console.log(val);
7 }
8
9 // ERROR...
We can fix this either by explicitly setting up the prototype chain link
between the Iterator.prototype object and our studentIterator object,
or an easier way is to just implement the Symbol.iterator method in the
studentIterator object to make it iterable:
1 Student.prototype[Symbol.iterator] = function () {
2 // code omitted to keep code example short
3
4 const studentIterator = {
5 next() {
6 // code omitted to keep code example short
7 },
8 [Symbol.iterator]() {
9 return this;
10 }
11 };
12
13 return studentIterator;
14 };
There’s one more improvement we can make to the above code example.
We have defined Symbol.iterator method in the Student.prototype
object, but it is enumerable, which is not ideal. We can make it non-
enumerable by defining it using the Object.defineProperty method, as
shown below:
1 Object.defineProperty(Student.prototype, Symbol.iterator, {
2 value: function () {
3 // copy the code inside the Symbol.iterator method from above
4 },
5 configurable: true,
6 writable: true
7 });
<ReplitEmbed src=”https://replit.com/@newlineauthors/generators-
example1” />
Infinite sequence
<ReplitEmbed src=”https://replit.com/@newlineauthors/generators-
example2” />
The code example above logs only 10 random numbers, but it is possible to
use the generator to generate an infinite number of random numbers. This is
possible because generator functions are evaluated lazily; their execution is
paused until a new value is requested.
In the above code example, we are consuming the generator using the next
method. Note that calling the generator function doesn’t execute it; instead,
it returns a generator object. The generator object is an iterator as well as an
iterable. So we can use it like an iterator by calling the next method on it.
Calling the next method on a generator object returns the values that the
generator yields. Multiple calls to the next method keep returning the value
that the generator yields until the generator object stops yielding the values.
At this point, we get an object with the done property set to true. However,
the generator function above can yield values infinitely.
Implementing iterators
<ReplitEmbed src=”https://replit.com/@newlineauthors/generators-
example3” />
Consuming values
While simple iterators only produce values, generators can also consume
values. We can pass a value to the generator using the next method. The
value passed to the generator becomes the value of the yield expression.
Consider the following code example of a generator consuming a value:
1 function* myGenerator() {
2 const name = yield "What is your name?";
3 yield `Hello ${name}!`;
4 }
5
6 const gen = myGenerator();
7 console.log(gen.next().value); // What is your name?
8 console.log(gen.next("John").value); // Hello John!
<ReplitEmbed src=”https://replit.com/@newlineauthors/generators-
example4” />
The first next method call yields the first value, i.e.,”What is your name?”.
At this point, the generator function pauses. The value of the yield
expression will be calculated depending on how we call the next method
again. If we pass an argument to the second call to the next method, that
argument will become the value of the yield expression. In the above code
example, the argument provided is the string “John”, so the value of the
first yield expression is “John”, and that is saved in the name constant
inside the generator function.
The second next method call provides a value for the first yield expression
and results in the generator function yielding the next value, which is also a
string in the code example above. The second yield value is calculated
using the value of the name constant. As a result, we get “Hello John!” as
the second value from the generator function.
:::note We didn’t pass any argument to the first next method call; this is
because any value provided to the first next method call is ignored. The
first next method call cannot provide a value for the first yield expression.
The value of the first yield expression will be provided by the second next
method call; similarly, the value of the second yield expression can be
provided by the third next method call, and so on. :::
The following is an example of a generator that produces random numbers
infinitely and allows us to pass the upper limit of the range in which the
random number should be produced. The number we pass is not included in
the range.
1 function* generatorRandomNumber(limit) {
2 while (true) {
3 const randomNumber = Math.floor(Math.random() * limit);
4 limit = yield randomNumber;
5 }
6 }
7
8 const randomNumGenerator = generatorRandomNumber(10);
9
10 console.log(randomNumGenerator.next());
11 console.log(randomNumGenerator.next(20));
12 console.log(randomNumGenerator.next(40));
13 console.log(randomNumGenerator.next(60));
14 console.log(randomNumGenerator.next(80));
15 console.log(randomNumGenerator.next(100));
<ReplitEmbed src=”https://replit.com/@newlineauthors/generators-
example5” />
Another cool thing about generators is that they allow us to delegate the
responsibility of producing values to other iterators, such as generators. To
delegate the responsibility, we need to use the yield* operator. This
operator takes any iterator and yields values from it until the iterator is
done. The following is an example of using the yield* operator to delegate
the responsibility of producing even or odd numbers to respective generator
functions:
1 function* evens() {
2 yield 2;
3 yield 4;
4 yield 6;
5 }
6
7 function* odds() {
8 yield 1;
9 yield 3;
10 yield 5;
11 }
12
13 function* printNums(isEven) {
14 if (isEven) {
15 yield* evens();
16 } else {
17 yield* odds();
18 }
19 }
20
21 for (const num of printNums(false)) {
22 console.log(num);
23 }
24
25 // 1
26 // 3
27 // 5
<ReplitEmbed src=”https://replit.com/@newlineauthors/generators-
example6” />
Further reading
We learned about iterables and iterators in the first lesson of this module.
An iterable is an object that implements the iterable protocol, and an iterator
is an object that implements the iterator protocol. The iterators we learned
about were synchronous.
In the above code example, we are using the async iterator directly instead
of using a construct like the for...of loop. Later in this module, we will
see how we can consume an async iterator using a loop.
Note in the above code example that the userAsyncIterator object inside
the fetchUsers function implements the Symbol.asyncIterator method to
implement the async iterable protocol. We discussed in the lesson about
iterators that each built-in iterator inherits from the Iterator.prototype
object and is an iterable itself. To make our custom iterators also iterables,
we can make our iterators inherit from the iterator.prototype object, or
we could implement the Symbol.iterator method in our iterator object to
make it an iterable. Similarly, async iterators inherit from the
AsyncIterator.prototype object, which is also a hidden global object, just
like the Iterator.prototype object. We could make our
userAsyncIterator object inherit from the AsyncIterator.prototype to
make it an iterable, or we could implement the Symbol.asyncIterator
method, as shown in the above code example.
Further reading
Let us rewrite the async iterator example in the previous lesson to use an
async generator:
1 async function* fetchUsers(userCount) {
2 // keep max user count to 10
3 if (userCount > 10) {
4 userCount = 10;
5 }
6
7 const BASE_URL = "https://jsonplaceholder.typicode.com/users";
8
9 for (let userId = 1; userId <= userCount; userId++) {
10 const response = await fetch(`${BASE_URL}/${userId}`);
11
12 if (response.ok) {
13 const userData = await response.json();
14 yield userData;
15 } else {
16 throw new Error("failed to fetch users");
17 }
18 }
19 }
20
21 async function getData() {
22 const usersAsyncGenerator = fetchUsers(3);
23
24 let userGeneratorResult = await usersAsyncGenerator.next();
25
26 while (!userGeneratorResult.done) {
27 console.log(userGeneratorResult.value);
28 userGeneratorResult = await usersAsyncGenerator.next();
29 }
30 }
31
32 getData();
<ReplitEmbed src=”https://replit.com/@newlineauthors/async-generators-
example1” />
:::
The async generators make it really easy to implement async iterators, and
this is how you would normally implement async iterators.
So far, we have consumed the async iterators directly by calling the next
method. The for...of loop, which helps us iterate over the iterable objects,
has a counterpart known as the for await…of loop that helps us iterate over
the async iterable. The following code example shows how we can rewrite
the getData function to use the for await...of loop:
1 async function getData() {
2 for await (const user of fetchUsers(3)) {
3 console.log(user);
4 }
5 }
The for await...of loop can only be used in a context where we can use
the await keyword, i.e., inside an async function and a module.
Further reading
:::note
:::
The debugger statement allows us to set up a point in our code where the
debugger can pause the execution of our code. This is like setting up
breakpoints in our code where the code execution can be paused, allowing
us to inspect the values of different variables in our code.
Run the following code example in the browser and enter the value “18”.
The code below works fine but gives incorrect output for the value “18”.
1 function isOldEnoughToDrive() {
2 const age = prompt("What is your age?");
3 let result;
4
5 debugger;
6
7 if (age === 18) {
8 result = "You are just about the right age to drive!";
9 } else if (age < 18) {
10 result = "Not allowed to drive";
11 } else if (age > 18) {
12 result = "Allowed to drive!";
13 } else {
14 result = "invalid age value provided";
15 }
16
17 const resultElm = document.querySelector("#result");
18 resultElm.innerHTML = result;
19 }
20
21 isOldEnoughToDrive();
1 <body>
2 <h2 id="result"></h2>
3 <script src="index.js"></script>
4 </body>
<ReplitEmbed src=”https://replit.com/@newlineauthors/debugger-
statement-example1” />
:::info
You can open the above code example in the browser using the VS Code’s
live server extension.
:::
The value “18” is a valid value, but we get “invalid age value provided” as
an output. Why is that? Let us debug that using the debugger statement.
:::note
The Firefox browser was used to show different debugging strategies in this
module. You can follow along using Firefox, Microsoft Edge, or the
Chrome browser.
:::
You might have noticed the debugger statement already added to the code.
It is only needed when debugging the code and can be removed after you’re
done debugging the code. But it didn’t do anything in the code; our code
didn’t pause at the debugger statement. Why is that? For the debugger
statement to pause the code execution, we need to have the browser
developer tools opened. Just open the browser’s developer tools.
Once opened, refresh the browser window, and you will notice the paused
code execution, as shown in the image below:
:::info You might need to drag the developer tools window to increase its
width in order to match the layout shown in the image above. The narrow
width of the window can show a different layout of the different areas
highlighted in the image above. :::
Now that the code execution is paused, we can focus on two areas of the
debugger: the debugger controls and the values of different variables in the
current scope; both areas are highlighted in the image above.
The debugger controls allow us to execute the code one line at a time,
making it easier for us to see how each line of code executes and how it
affects the values of different variables.
:::info You can hover over each button in the debugger controls to know
what it does. :::
You can also see the call stack above the “Scopes” section in the image
above. This allows us to view how the current function was called. We can
also hover over different variables in our code to view their values.
:::info You can change the values of different variables in the “Scopes”
section by double-clicking on the value. This allows us to see how our code
behaves if the values of variables in the current scope are changed. :::
Let us debug why our code doesn’t work when the value is “18”. Note the
value of the age variable (hover over it or look at the “Scopes” section);
you will see that its value is a string and not a number. That means the
prompt function, which takes the user’s input, returns a string. So when we
get to the first if condition, i.e., age === 18, it doesn’t evaluate to true.
Can you guess why? Because comparing a string with a number using the
triple equals (strict equality) operator always evaluates to false and you
probably knew that, but if you didn’t, the debugger helped you know that
the value of age is a string and you are comparing it to a number, so it did
help you better understand your code.
This was a simple example to show you how the debugger statement can be
used to debug our code. The debuggers built into browsers are really
powerful, and it is worth exploring every feature of them to enhance your
debugging skills.
In the previous lesson, we used the debugger statement to pause the code
execution and debug our code. There’s another way to pause the code
execution, and that is by using breakpoints. A breakpoint acts just like the
debugger statement, but the difference is that we don’t have to write any
special keywords in our code. Instead, we open the JavaScript code in the
browser’s developer tools and set breakpoints in the browser’s developer
tools.
As shown in the previous lesson, our JavaScript code was opened in the
“Debugger” tab. In Chrome, the corresponding tab is named “Sources”. The
overall functionality of the debugger is more or less the same for both
browsers. The following is a screenshot of the “Sources” tab in the Chrome
browser containing our JavaScript code:
Now, instead of using the debugger statement to pause the code execution,
let us use the breakpoints. We will use the same example as in the previous
lesson but without the debugger statement. To set breakpoints, we first need
to open our code in the browser, open the developer tools, and open the
“Sources” or “Debugger” tab if you are using the Firefox browser or
Chrome (other browsers will also have a similar tab).
:::info
If you are following along from the previous lesson, you probably already
have the code opened in the browser; if not, you can open the code example
in the previous lesson in the browser. You can use VS Code’s live server
extension to open the code example.
:::
Just click on the line number at which you want to set the breakpoint and
refresh the browser window. Just like with the debugger statement, when
the execution reaches the breakpoint set in our code, the code execution will
be paused, and from there on, we can use the different features provided by
the browser debugger to debug our code.
Further reading
Visual Studio Code (VS Code) is one of the most commonly used editors
these days due to the features and flexibility that it provides with the help of
the many extensions that are available to use with it. Among the many
features that VS Code provides, one is the built-in debugger that allows us
to debug our code within VS Code. Apart from the built-in debugger, there
are many extensions available for debugging code written in different
languages.
To use VS Code to debug our code, open the same code example in VS
Code that we have been working on within the last two lessons. Once
opened, create a folder named “.vscode” in the folder containing our code
(HTML and JavaScript files). Inside the “.vscode” folder, create a file
named “launch.json” and paste the following JSON into this file:
1 {
2 "version": "0.2.0",
3 "configurations": [
4 {
5 "type": "chrome",
6 "request": "launch",
7 "name": "Launch Chrome against localhost",
8 "file": "${workspaceFolder}/index.html"
9 }
10 ]
11 }
The red dot in the image above is a breakpoint added by clicking on line
number 5.
After this, open the “Run and Debug” option in VS Code, as shown in the
image below:
screenshot of Run and Debug option in VS Code
Once the “Run and Debug” window opens, as shown in the image above,
click on the green play button at the top in the image above. This will open
up the Chrome browser, and the debugger will pause the code execution
when it reaches the breakpoint. As shown in the image above, the
breakpoint is added at line 5, so the debugger will pause the execution after
taking user input. Depending on where you added the breakpoint, the code
execution will be paused whenever it reaches that point.
The following image shows the state of VS code when code execution is
paused at the breakpoint:
screenshot of VS code debugger
You can see the debugger controls at the top, which works similarly to the
debugger controls in the browser. The highlighted line shows the point
where the code execution paused, and there is a call stack and the variables
in the current scope in the left sidebar. We can advance the debugger
forward or resume it using the debugger controls and see the flow of
execution and values of different variables in the scope to debug our code.
Further reading:
There is a lot more that you can do with the debugger in VS Code. The
“launch.json” file that we created manually can be created automatically by
VS Code with the click of a button. This and other things possible with the
VS Code debugger are explained in the VS Code documentation:
Despite all its quirks, JavaScript is an amazing language to learn. The aim
of this course is not to cover the JavaScript language in its entirety. Instead,
it aims to cover the core topics that are often not understood well enough,
especially by beginners. There is so much about JavaScript that couldn’t be
covered in this course. But with a solid understanding of its core topics, one
can surely continue learning it further.
Learning JavaScript opens the door for working not only on the frontend
using modern frontend frameworks but also on the backend using
technologies like NodeJS.
Next Steps
Following are some of the resources that can be used to continue your
journey of learning JavaScript:
With that, I shall thank you for investing your time in this course, and
hopefully it was able to help you gain a deeper understanding of the
JavaScript language.