Advanced Javascript Unleashed
Advanced Javascript Unleashed
Advanced Javascript Unleashed
© 2024 Fullstack.io
Published by \newline
Th
Th
tt
Contents
What is JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Course Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Running code examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Who is this course for? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
History of JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Standardization of the JavaScript language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Ecma International, TC39 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Proposals process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Track upcoming features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
How is ECMAScript versioned? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
JIT Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
JavaScript Engine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Types of execution contexts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Execution context phases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Stack overflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Automatic garbage collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Hoisting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
“var” declarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Function declarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Function declarations inside blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Class declarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Temporal Dead Zone (TDZ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Function and class expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Common misconception about hoisting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Lexical scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Avoid polluting the global scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Implicit globals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Shadowing declarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Function parameter scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Function expression name scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
CONTENTS
Block Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Module Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Scope Chain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Coercion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
ToPrimitive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
ToNumber . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
ToString . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
ToBoolean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Summary of abstract equality operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Addition Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Relational operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Closures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
What is a closure? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
How do closures work? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
How are different scopes linked? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
What causes this problem? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
How to resolve this problem? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Prototypes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
How are objects linked? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
The “prototype” property . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Getting prototype of any object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Object.prototype - parent of all objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
“Function” function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Problems with __proto__ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Object.create method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
Null prototype object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
ES2015 classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Symbol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
CONTENTS
Wrap up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
What is JavaScript
Course Overview
JavaScript is arguably the most widely used programming language on the planet, and there is a
vast amount of content available for learning JavaScript. The problem is that not all the content on
the internet does a good job of explaining the complex or confusing concepts of JavaScript.
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.
This course will not only provide easy-to-understand explanations of fundamental JavaScript topics
like hoisting, coercion, event loop, etc., but will also cover advanced topics like promises and async-
await syntax in a way that will be easy for the students to understand.
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.
This course is unlike online articles/blogs and many JavaScript video courses that either fail
to provide in-depth, easy-to-understand explanations of JavaScript topics that are not easy for
beginners to understand or do not cover all the necessary topics (fundamental to advanced) that
a good JavaScript developer must understand.
This course combines fundamental but confusing JavaScript topics in a single course that aims to
provide a solid understanding to the students and cover all the topics that are key to understanding
JavaScript in depth.
JavaScript is currently the most widely used programming language. With the rise of modern
front-end frameworks and platforms like Node.js, it seems that JavaScript will be the most used
programming language for the foreseeable future.
It is the programming language of the web, most commonly used to add interactivity to web
pages. With the help of technologies like Node.js, which allow us to write JavaScript outside the
browser environment, software developers can now write full-stack applications by learning just
one programming language.
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.
What is JavaScript 3
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.”
This language was supposed to be part of the Netscape Navigator 2 browser, but just before the
release of the browser, LiveScript was renamed to “JavaScript.”
When Netscape released its Netscape Navigator 3 browser, Microsoft came up with its web
browser known as Internet Explorer 3, which included Microsoft’s implementation of the JavaScript
language.
This presented a problem: there were now two separate implementations of the JavaScript language.
There was a need for a standards body that was responsible for the advancement of the JavaScript
language.
JavaScript community. Mostly people from big companies like Google, Microsoft, etc. are part of
this group.
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
Proposals at this stage are discussed and developed by the community (including non-committee
members), and if the proposal still has enough interest and is thought to have potential benefit if
added to the specification, initial specification language and API are developed and discussed in the
TC39 meeting. Provided everything goes well, the proposal is moved to the next stage.
³https://tc39.es/
⁴https://github.com/tc39
What is JavaScript 5
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
Proposals at this stage are almost ready to be included in the ECMAScript specification.
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
At this stage, the proposal is complete and is ready to be included in the ECMAScript specification
and ultimately be added to the JavaScript language.
A pull request is generated to the ecma262 GitHub repository to include it in the specification. Once
the pull request is approved, the proposal is part of the specification and ready to be implemented
in the JavaScript engines that still need to implement it.
For further details of this process, refer to a process document⁵ on TC39’s website, which explains
the various stages through which proposals pass through.
The answer to this question lies in TC39’s GitHub repository⁶, which contains meeting notes⁷ and
proposals⁸ that are at various stages of the proposal process described in the earlier lesson.
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.
JIT Compiler
Just-in-time (JIT) compilation is a technique used by many modern JavaScript engines to increase
the execution speed of the JavaScript code.
⁶https://github.com/tc39
⁷https://github.com/tc39/notes
⁸https://github.com/tc39/proposals
What is JavaScript 7
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.
One way to optimize performance is to compile byte code into machine code, which executes faster
than the byte code. The JavaScript engine identifies the “hot” parts of the code to do this - parts that
are being executed frequently.
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.
So how is the JIT compiler different from a traditional compiler used by languages like C++? Unlike
traditional compilers, which compile the code ahead of time, the JIT compiler compiles the code at
runtime while the code is being executed.
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.
So, coming back to the question: is JavaScript a compiled or interpreted language? It is safe to say
that it is both - compiled as well as an interpreted language.
We don’t necessarily need to understand the nitty-gritty details of how exactly the JavaScript code
that we write is executed, but to develop a good understanding of the language; it is important
to have a basic understanding of how our JavaScript code gets transformed into something that a
machine can understand and execute.
It is also important to understand what different things come into play while our code is executing;
concepts like “execution context,” “call stack,” etc. are crucial to understanding the JavaScript
language’s runtime behavior and being able to work with it and debug it efficiently.
JavaScript Engine
To execute JavaScript code, we need another software known as a JavaScript engine. This engine
contains all the necessary components to transform the code into something the machine can
execute.
Different browser vendors typically create JavaScript engines; each major vendor has developed a
JavaScript engine that executes the JavaScript code in their browser.
The following table shows some major browsers and their JavaScript engines.
Browser Engine
Google Chrome V8
Edge Chakra
Mozilla Firefox Spider Monkey
Safari JavaScriptCore
What is JavaScript 8
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.
The following image shows the high-level overview of the execution pipeline of the V8 engine:
process stages
The JavaScript engine is complicated software that contains lots of steps and components that are
used to transform and execute the JavaScript code, but the above image shows a simplified version
of the execution pipeline of the 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.
What is JavaScript 9
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.”
The following JavaScript code:
1 [
2 { type: "keyword", value: "function" },
3 { type: "identifier", value: "add" },
4 { type: "openRoundParen", value: "(" },
5 { type: "identifier", value: "num1" },
6 { type: "identifier", value: "num2" },
7 { type: "closeRoundParen", value: ")" },
8 { type: "openCurlyParen", value: "{" },
9 { type: "keyword", value: "return" },
10 { type: "identifier", value: "num1" },
11 { type: "addOperator", value: "+" },
12 { type: "identifier", value: "num2" },
13 { type: "closeCurlyParen", value: "}" }
14 ];
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.
V8 also passes the generated bytecode through some optimizers, which perform some optimizations
to ensure efficient execution of the bytecode by the Bytecode Interpreter.
⁹https://en.wikipedia.org/wiki/Abstract_syntax_tree
¹⁰https://astexplorer.net/
What is JavaScript 10
Compiler
While the bytecode is executed, the JavaScript engine collects information about the code being
executed. The engine then uses this information to optimize the code further.
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.
However, the optimized native machine code sometimes has to be deoptimized back to the bytecode
generated by the Bytecode Generator because of the way the code was written.
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.
Consider the following code:
1 function print(obj) {
2 console.log(obj);
3 }
1 print({ a: 1, b: 2, c: 3 });
2 print({ a: 1, c: 3 });
3 print({ b: 2 });
This means that if the print function is called multiple times with objects with the following shape:
1 { a: 1, b: 2, c: 3 }
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.
The optimized native machine code is generated using the information collected during the
execution of the JavaScript code. The native machine code requires certain checks to ensure that
the assumptions made during the generation of the native machine code are not violated. If the
checks fail, the JavaScript engine has to execute the bytecode instead of the native machine code.
This process is called deoptimization.
References:
The following resources can be used to learn more about how the JavaScript code is executed:
• How JavaScript Works: Under the Hood of the V8 Engine - (freeCodeCamp blog)¹³
• What does V8’s ignition really do? - (stackoverflow post)¹⁴
• Ignition - an interpreter for V8 - (youtube video)¹⁵
• Blazingly fast parsing, part 1: optimizing the scanner - (V8 blog)¹⁶
• Overhead of Deoptimization Checks in the V8 JavaScript Engine - (paper by Dept. of Computer
Engineering, University of California)¹⁷
:::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 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
In the case of a global context, there is no outer environment, so reference to the outer environment
is set to null, but in the case of a function context, the value of this depends on how the function
is called, so the value of this is set appropriately.
:::
What is JavaScript 13
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.
Consider the following code:
The execution context for the above code during the creation phase can be conceptually visualized
as shown in the image below:
What is JavaScript 14
Execution phase
As mentioned earlier, after the creation phase, different variables in the execution context are yet to
be assigned their respective values. Assignments are done during the execution phase, and the code
is finally executed.
References
Following resources can be used to learn more about the execution context in JavaScript:
A call stack is a structure that is used internally by the JavaScript engine to keep track of the piece
of code that is currently executing. The call stack is simply a stack²² data structure that aids in the
execution of the JavaScript code by keeping track of currently executing code. You can also think of
a call stack in JavaScript as a collection of execution contexts.
Before executing any JavaScript code, a global execution context is created and pushed on the call
stack. This can be easily visualized using the debugger in the browser developer tools.
:::note
¹⁹https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0
²⁰https://www.freecodecamp.org/news/execution-context-how-javascript-works-behind-the-scenes/
²¹https://www.youtube.com/watch?v=Fd9VaW0M7K4
²²https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
What is JavaScript 16
The debugger keyword simply creates a breakpoint, forcing the debugger to stop at the line
containing the debugger keyword. We will cover debugging in a later chapter.
:::
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:
Consider the following code:
1 function bar() {
2 console.log("hello world");
3 }
4
5 function baz() {
6 bar();
7 }
8
9 function foo() {
10 baz();
11 }
12
13 foo();
Before the execution of the above code, the global execution context is pushed on the call stack.
What is JavaScript 17
callstack
What is JavaScript 18
As soon as the foo function is called, a new entry is added to the call stack for the execution of the
foo function.
What is JavaScript 19
callstack
What is JavaScript 20
The foo function contains a call to the baz function. So, another entry is pushed on the call stack for
the baz function call.
What is JavaScript 21
callstack
What is JavaScript 22
The baz function contains a call to the bar function. So, another entry is pushed on the call stack for
the bar function call.
What is JavaScript 23
callstack
What is JavaScript 24
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.
So what happens if we create a function that just calls itself?
1 function foo() {
2 foo();
3 }
Remember what happens when a function is called? A new entry is added to the call stack. In the
case of the above function that just calls itself and never finishes executing, we are just adding the
new entries in the call stack without ever removing any entries. This is infinite recursion, and this
leads to an error known as stack overflow. This error is thrown when the call stack gets filled up
to its limit and can no longer hold more 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.
²³https://stackoverflow.com/
²⁴https://v8.dev/blog/pointer-compression#value-tagging-in-v8
What is JavaScript 25
JavaScript values in V8 are represented as objects and allocated on the V8 heap, no matter
if they are objects, arrays, numbers, or strings. This allows us to represent any value as a
pointer to an object.
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.
JavaScript engines are complicated softwares that are heavily optimized. It is unreasonable to assume
that they all just follow the simple rule of primitives go on the stack and objects on the heap.
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
The term “hoisting” is mostly associated with function declarations and variables declared with the
“var” keyword. Let’s take a look at how hoisting is associated with the “var” variables.
Consider the following code example:
1 console.log(result); // undefined
2 var result = 5 + 10;
1 function print(obj) {
2 console.log(obj; // error
3 }
4
5 console.log("hello world");
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:
case of function declarations, the function’s name is registered as a variable in the scope containing
the function declaration, and it is initialized with the function itself.
In the above code example, the startCar is registered as a variable in the global scope, and it is
assigned the function. Unlike the “var” variables, there is no initialization with the undefined value
in the case of function declarations.
It is hard to see how hoisting can be a useful feature for a programming language until we see the
hoisting of function declarations. To be able to call a function before or after the function declaration
is really useful and frees the developer from arranging the code in such a way that every function
declaration comes before it is called. This helps in code organization, as we can declare functions
together either at the top or at the bottom of the code file and call them from anywhere we want in
that file.
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:
³⁰https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode
Hoisting 30
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:
³¹https://262.ecma-international.org/13.0/#sec-block-level-function-declarations-web-legacy-compatibility-semantics
Hoisting 31
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
1 function process() {
2 var fnInsideBlockVar;
3
4 if (true) {
5 let fnInsideBlock = function fnInsideBlock() {
6 console.log("I am inside a block");
7 };
8
9 console.log("processing...");
10
11 fnInsideBlockVar = fnInsideBlock;
12 }
Hoisting 32
13
14 fnInsideBlockVar();
15 }
16
17 process();
Further reading
• What are the precise semantics of block-level functions in ES6? - (stackoverflow post)³²
• Why does block assigned value change global variable? - (stackoverflow post)³³
• Function declaration in block moving temporary value outside of block? - (stackoverflow
post)³⁴
Class declarations
Like function declarations, class declarations are also hoisted, but they are hoisted differently
compared to the function declarations.
³²https://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6
³³https://stackoverflow.com/questions/61191014/why-does-block-assigned-value-change-global-variable
³⁴https://stackoverflow.com/questions/58619924/function-declaration-in-block-moving-temporary-value-outside-of-block
Hoisting 33
While we can access a function declaration before its declaration, we cannot do the same in the
case of class declarations. Doesn’t that mean that the class declarations aren’t hoisted? No, they are
hoisted, but differently.
Let us first verify that class declarations are indeed hoisted with the help of the following code
example:
³⁵https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal_dead_zone_tdz
Hoisting 34
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.
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” />
• Global scope
• Function scope
• Block scope
• Module scope
Before discussing the above-mentioned types of scopes in JavaScript, we need to understand what
“lexical” scope is.
Lexical scope
In JavaScript, the scope of different identifiers (variables, functions, etc.) is determined at compile
time. During compilation, the JavaScript engines determine the scope of different identifiers de-
clared in the code by analyzing the code structure. This means that before the step-by-step execution
of the JavaScript code starts, JavaScript engines determine the scopes of different declarations in the
code.
Scopes can be nested within other scopes, with each nested scope having access to the outer or parent
scope.
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.
This type of scope, which is determined at compile time by analyzing the code structure, is known
as lexical scope. JavaScript is not the only language that has a lexical scope. Other languages, like
Java, also have a lexical scope.
:::info
Lexical scope is also known as “static” scope. An alternative type of scope is Dynamic scope³⁶.
:::
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.
Variables created using the var keyword or function declarations declared in the global scope are
added as properties on the window object. The following code example verifies this claim:
If the todoList was declared with let or const, it wouldn’t have been added as a property on the
window object, but it would still be a global variable. Similarly, if the emptyTodoList was a function
expression instead of a function declaration, and if the identifier referring to this function expression
was declared using let or const, it also wouldn’t be part of the window object as its property. Instead,
it would just be a global function expression.
³⁶https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scope_vs._dynamic_scope
Scope 38
Implicit globals
JavaScript as a language has many quirks. One of those is the implicit creation of global variables.
Whenever there is an assignment to an undeclared variable, JavaScript will declare that undeclared
variable as a global variable. This is most likely a mistake by the programmer, and instead of
throwing an error, javaScript hides this by automatically declaring a global variable by the same
name.
Scope 39
1 function printSquare(num) {
2 result = num * num;
3 console.log(result); // 64
4 }
5
6 printSquare(8);
7
8 console.log("implicit global: " + result); // WHAT??!!
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.
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.
Although this is supported by most browsers, this feature shouldn’t be relied upon, and we should
always use standard mechanisms to target HTML elements. Functions like getElementById or
querySelector should be used instead of relying on this behavior.
³⁷https://html.spec.whatwg.org/multipage/nav-history-apis.html#named-access-on-the-window-object
Scope 40
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
The function scope refers to the area of the code within the function body. The function scope
starts from the opening curly parenthesis of the function body and ends before the closing curly
parenthesis. Declarations inside the function scope are limited to that function’s scope and cannot
be directly accessed by the code outside that function.
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.”
Consider the following code example:
⁴⁰https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters
⁴¹https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
⁴²https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters
Scope 42
If the parameters are simple, they behave like they are declared in the function’s local scope, but if
the parameters are non-simple, they are declared in their own scope. Non-simple parameter scope
can be thought of as between the function scope and the scope containing the function.
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:
Scope 43
The following code example proves that the non-simple parameters indeed exist in a different scope
than the function’s local scope:
<ReplitEmbed src=”https://replit.com/@newlineauthors/function-scope-example4”>
The paramScope function in the code example above has a non-simple parameter list; the first
parameter, arr has an array as a default value, whereas the second parameter, buff has a function
as its default value.
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 let fn = function () {
2 // code ...
3 };
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:
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.
Prior to the addition of block-scoped let and const in the JavaScript language, variables defined
with the var keyword in a block were accessible outside that block. This is because the variables
declared with the var keyword have function scope. However, the variables declared using let, or
⁴³https://stackoverflow.com/questions/61208843/where-are-arguments-positioned-in-the-lexical-environment/
⁴⁴https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/apA.md#implied-scopes
⁴⁵https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function#named_function_expression
Scope 46
the constants declared using const are scoped to the block in which they are defined unless they are
declared in the global scope, which makes them global variables.
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
Different scopes can be nested inside other scopes, creating a chain of scopes. This is known as a
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.
When an identifier is encountered in a particular scope, the JavaScript engine will look for the
declaration of that variable in the parent scope of the current scope. If the declaration is not found
in the parent scope, then the JavaScript engine will look for that variable declaration in the outer
(parent) scope of the parent scope. This process of traversing the scope chain will continue until the
global scope is reached and there are no other scopes to look into for the declaration.
Consider the following code example:
⁴⁶https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#other_differences_between_modules_and_standard_scripts
⁴⁷https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
Scope 47
scope chain
The scope chain enables the lookup of identifiers that are not declared in the current scope.
has no choice but to traverse the scope chain at runtime to determine what scope that identifier is
declared in.
Further reading
⁴⁸https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/ch3.md
Coercion
Coercion in JavaScript is the conversion of one type of value into another type of value.
MDN defines coercion⁴⁹ as:
Type coercion is the automatic or implicit conversion of values from one data type to
another (such as strings to numbers).
According to MDN’s definition, if the conversion of values is implicit, then it is coercion, whereas
type conversion can either be implicit or explicit.
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:
And if the type conversion is implicit, where the developer expressed no intention to convert any
value into another value, then it is implicit type conversion or coercion. The following code is an
example of coercion:
Whenever JavaScript sees a value of one type in a context that expects a value of a different type, it
tries to coerce or convert the value into the expected type. In the above code example, "50" is the
unexpected value type because the operation is subtraction. The subtraction is between numbers, not
between a string and a number. So "50", being an unexpected value, gets converted into a number,
i.e. 50.
However, some might argue that any type of conversion in a dynamically typed language can be
considered coercion. Moreover, the difference between implicit and explicit type conversion depends
on how one views implicit and explicit type conversion.
Having said that, what’s important is that we understand the process of type conversion in JavaScript.
The goal of this module is to help you understand how JavaScript converts one type of value into
another and make type conversion (implicit or explicit) less scary.
Coercion is one of those topics that many JavaScript developers generally misunderstand, and this
is because most online resources advise staying away from coercion and presenting it as a topic
⁴⁹https://developer.mozilla.org/en-US/docs/Glossary/Type_coercion
Coercion 51
that should be avoided instead of taking the time to understand it and take advantage of it where
possible.
What makes coercion scary for many JavaScript developers, especially beginners, is the need for
more understanding of this topic. Coercion is presented as a feature of JavaScript that is better
avoided than understood.
Coercion is one of the core topics of JavaScript, and its understanding is key to understanding
JavaScript in depth. No matter how many online resources tell you to avoid this topic, it is
unavoidable if you work with JavaScript. Instead of avoiding it, why not make an effort to
understand it? With this in mind, in this module, we will take a deeper look at this topic, and
hopefully, by the end of this module, you will have a solid understanding of type coercion.
Coercion is one of the core topics of JavaScript, and its understanding is key to understanding
JavaScript in depth. No matter how many online resources tell you to avoid this topic, it is
unavoidable if you work with JavaScript. So, instead of avoiding it, why not try to understand
it? With this in mind, in this module, we will take a deeper look at this topic, and hopefully, by the
end of this module, you will have a solid understanding of type coercion.
To understand coercion in-depth, we need to understand how JavaScript goes about converting one
value into another type of value. We need to understand what algorithms or steps JavaScript takes
to perform type conversion.
To deep dive into the world of coercion, let us understand the following:
• Abstract operations
• Abstract equality operator (==)
• Addition operation (+)
• Relational operators (<, >, <=, >=)
Understanding the above-mentioned topics will help us lay the foundation for understanding the
process of type conversion in JavaScript. So, without further ado, let us begin by understanding the
first item on our list, i.e., abstract operations.
The ECMAScript specification has documented several mechanisms that are used by the JavaScript
language to convert one type of value into another type of value. These mechanisms are known
as “abstract operations”; abstract in the sense that these are not some real functions that can be
referred to or called by the JavaScript code; instead, they are just algorithmic steps internally used
by the language to perform type conversion.
These abstract operations are written in the specification as though they were actual functions.
For example, operationName(arg1, arg2, ...), but the specification clarifies that the abstract
operations are algorithms rather than actual functions that can be invoked.
There are many abstract operations⁵⁰ mentioned in the ECMAScript specification, but some of the
common ones that come into play when dealing with coercion are mentioned below:
⁵⁰https://262.ecma-international.org/13.0/#sec-abstract-operations
Coercion 52
• ToPrimitive
• ToNumber
• ToString
• ToBoolean
Although the names of the above-mentioned abstract operations are self-descriptive, let us under-
stand how exactly they aid in type conversion.
ToPrimitive
The ToPrimitive⁵¹ abstract operation is used to convert an object to a primitive value. This operation
takes two arguments:
OrdinaryToPrimitive
This operation invokes another abstract operation known as OrdinaryToPrimitive⁵² to do the actual
conversion, and it also takes two arguments:
ToPrimitive abstract operation invokes the OrdinaryToPrimitive abstract operation, passing in the
object, that is to be converted into a primitive value, as the first argument, and the second argument
hint is set based on the value of preferredType argument as described below:
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( )
The toString method is used to convert an object into its string representation. The default behavior
of the toString method is to convert objects in the following (not so-useful) form:
⁵¹https://262.ecma-international.org/13.0/#sec-toprimitive
⁵²https://262.ecma-international.org/13.0/#sec-ordinarytoprimitive
Coercion 53
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.
Prefer string
If the hint argument is “string”, then the OrdinaryToPrimitive abstract operation first invokes
the toString method on the object. If the toString method returns a primitive value, even if that
primitive value is not of the string type, then that primitive value is used as a primitive representation
of the object.
If the toString method doesn’t exist or doesn’t return a primitive value, then the valueOf method
is invoked. If the valueOf method returns a primitive value, then that value is used; otherwise, a
TypeError is thrown, indicating that the object couldn’t be converted to a primitive value.
1 const obj = {
2 toString() {
3 console.log("toString invoked");
4 return "hello world";
5 },
6 valueOf() {
7 console.log("valueOf invoked");
8 return 123;
9 }
10 };
11
12 console.log(`${obj}`);
13 // toString invoked
14 // hello world
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
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
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
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 ...
Coercion 57
Prefer number
If the hint argument is “number”, then the OrdinaryToPrimitive abstract operation first invokes the
valueOf method and then the toString method, if needed.
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
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”.
What will happen if the valueOf method returns a boolean value? It is a primitive value. It is not a
number but still a primitive value. So JavaScript should accept it as a primitive representation of the
obj object, right?
1 const obj = {
2 valueOf() {
3 console.log("valueOf invoked");
4 return true;
5 }
6 };
7
8 console.log(obj + 1);
9 // valueOf invoked
10 // 2
No preference
When the ToPrimitive abstract operation is called without the preferred type or hint, or if the hint
is set to “default”, then this operation generally behaves as if the hint were “number”. So, by default,
the ToPrimitive prefers the conversion to number type.
However, the objects can override this default ToPrimitive behavior by implementing the Sym-
bol.toPrimitive⁵⁴ function. This function is passed a preferred type as an argument, and it returns
the primitive representation of the object.
⁵⁴https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive
Coercion 59
1 const obj = {
2 [Symbol.toPrimitive](hint) {
3 if (hint === "string") {
4 return "hello";
5 } else {
6 return 123;
7 }
8 }
9 };
10
11 console.log(`${obj}`); // hello
12 console.log(obj + 1); // 124
ToNumber
The ToNumber⁵⁵ abstract operation is used whenever JavaScript needs to convert any non-number
value into a number.
The following table shows the results of this abstract operation applied to some non-number values:
⁵⁵https://262.ecma-international.org/13.0/#sec-tonumber
Coercion 61
Value ToNumber(value)
”” 0
“0” 0
“-0” -0
” 123 “ 123
“45” 45
“abc” NaN
false 0
true 1
undefined NaN
null 0
As far as objects are concerned, the ToNumber abstract operation first converts the object into a
primitive value using the ToPrimitive abstract operation with “number” as the preferred type, and
then the resulting value is converted into a number.
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...
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”
⁵⁶https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
⁵⁷https://262.ecma-international.org/13.0/#sec-tostring
Coercion 62
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
The ToBoolean⁵⁸ abstract operation is used to convert a value into a boolean value.
Unlike the above-mentioned abstract operations, this operation is simply a lookup of whether a value
is a falsy⁵⁹ value. If it is, we get false as a return value; for all other values, this operation returns
true.
• false
• 0, -0, 0n
• undefined
• null
• NaN
• ””
As mentioned earlier, there are many abstract operations⁶⁰ mentioned in the ECMAScript specifica-
tion that are used for type conversion; in this lesson, we have only discussed the common ones.
Further reading
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.
This operator is infamous because many resources online, and JavaScript developers, in general,
discourage its use because of its coercive behavior.Instead of blindly ignoring the double equality
operator, we should try to understand how it behaves, and then we can decide for ourselves whether
we want to not use it at all in our code or use it where it is safe to use.
⁵⁸https://262.ecma-international.org/13.0/#sec-toboolean
⁵⁹https://developer.mozilla.org/en-US/docs/Glossary/Falsy
⁶⁰https://262.ecma-international.org/13.0/#sec-abstract-operations
⁶¹https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#number_coercion
⁶²https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion
⁶³https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean#boolean_coercion
⁶⁴https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/types-grammar/ch4.md
Coercion 63
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 compari-
son⁶⁶.
• 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.
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.
⁶⁵https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal
⁶⁶https://tc39.es/ecma262/multipage/abstract-operations.html#sec-isstrictlyequal
Coercion 64
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
Although the coercive behavior of the abstract equality operator is predictable, as explained above,
people often fall into a trap because of how they use this operator in the if statement conditions.
Consider the following code example:
Coercion 65
1. One operand is an object, and the other one is a boolean, so according to step 10 of the
isLooselyEqual⁶⁸ abstract operation, if one operand is a boolean value, convert it into a number
using the ToNumber abstract operation. So our condition would become:
1 someVal == 1;
2. After coercing a boolean value into a number, we have a comparison between an object
and a number. According to step 12 of the isLooselyEqual abstract operation, the object
someVal would be converted into a primitive value using the ToPrimitive abstract operation,
passing “number” as the preferred type. The default primitive representation of object literals
is "[object Object]", so our condition after coercion would become: js "[object Object]"
== 1;
3. Now, we have a comparison between a string and a number. According to step 6 of the
isLooselyEqual abstract operation, if one operand is a string and the other one is a number,
convert a string into a number. Converting "[object Object]" into a number will give us
NaN⁶⁹. So our condition would become:
1 NaN == 1;
4. After coercing three times, we have a comparison between a NaN value and a number. They
are not equal to each other. (NaN value is not equal to any other value, including itself.). So our
condition fails to evaluate as true.
⁶⁷https://developer.mozilla.org/en-US/docs/Glossary/Truthy
⁶⁸https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal
⁶⁹https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN
Coercion 66
The purpose of the above discussion is to understand that checking if a value is true or false using
the abstract equality operator doesn’t always work as one might expect. It is easy to blame the
abstract equality operator in such cases. Still, the fact is that those who write such code need help
understanding or remembering how the abstract equality operator works.
In such cases where we want to check if a value is truthy or falsy, instead of using the abstract
equality operator, it is enough to take advantage of the coercive behavior of the if statement. What
I mean is that the if condition in the above code should be written as:
1 if (someVal) {
2 // code
3 }
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.
The working of this operator is based on the ApplyStringOrNumericBinaryOperator⁷². The way this
works is that if any or both operands are non-primitive values, they are converted into primitive
values using the ToPrimitive abstract operation, and no preferred type is specified. As a result, the
“number” is the preferred type in this case because that is the default behavior of the ToPrimitive
abstract operation.
After checking for non-primitive operands and coercing them, if any, into primitive values, the next
step is to check if either or both operands are strings. If that’s the case, then the non-string operands,
if any, are coerced into strings, and string concatenation is performed.
If neither operand is a string, then the addition is performed after coercing non-number operands
into numbers.
⁷⁰https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/types-grammar/ch4.md#-boolean-gotcha
⁷¹https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html#sec-if-statement
⁷²https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-applystringornumericbinaryoperator
Coercion 67
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.
If either operand is an object, it is converted into a primitive value with “number” as the preferred
type. If both operands are strings, then they are compared using their Unicode code points. If not
strings, then the operands are generally converted into numbers and then compared.
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 0 == false
2
3 "" == false
4
5 0 == []
6
7 [123] == 123
8
9 [1] < [2]
⁷³https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islessthan
Coercion 68
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
2. Now the types are equal, so the strict equality comparison is performed, i.e., 0 === 0, giving
us true as an output.
"" == 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:
⁷⁴https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber
Coercion 69
1 0 == 0
3. The types are equal, so the strict equality comparison is performed, i.e., 0 === 0, giving us true
as an output.
0 == []
1. The array is converted into a primitive value using the ToPrimitive abstract operation. As the
abstract equality operator prefers number comparison, the array is converted into a primitive
value with a number as the preferred type. An empty array, when converted into a primitive
value, outputs an empty string. So the expression becomes:
1 0 == ""
2. Next, the string will be converted into a number. An empty string converted into a number
outputs 0. So the expression becomes:
1 0 == 0
3. The types are equal, so the strict equality comparison is performed, i.e., 0 === 0, giving us true
as an output.
[123] == 123
1. We have a comparison between an array and a number. So, the array is converted into a
primitive value using the ToPrimitive abstract operation, with number as the preferred type.
The valueOf method will be invoked first, as the preferred type is a number. But we know
that the default implementation of the valueOf method simply returns the object on which it
is called. So, the toString is invoked next. For Arrays, the toString method returns an empty
string for empty arrays; for an array like [1, 2, 3], it returns the contents of the array as a
string, joined by commas, i.e., "1,2,3". Each array element is coerced into a string and then
joined by comma(s).
In our case, we have a single element in an array, i.e., [123], so it will be coerced into "123".
So the expression becomes:
1 "123" == 123
2. Next, the string will be converted into a number. So the expression becomes:
1 123 == 123
3. The types are equal, so the strict equality comparison is performed, i.e., 123 === 123, giving
us true as an output.
:::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. :::
Coercion 70
1. This is a comparison between two objects. Both arrays are converted into primitive values
using the ToPrimitive abstract operation, with number as the preferred type. As explained
in the previous example, the toString will eventually be called to convert both arrays into
primitive values, giving us "1" and "2" as output, respectively. So the expression becomes:
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.
[] == ![]
1. In this comparison, we have two operators: the abstract equality operator and the Not (!)⁷⁵
operator. The Not operator has a higher precedence⁷⁶ than the equality operator, so the sub-
expression ![] is evaluated first.
The Not operator converts true into false, and vice versa. But here it is used with a non-boolean
value. So what happens when JavaScript sees a value of one type in a context where it expects
a value of a different type? Coercion! So [] will be coerced into a boolean value, as boolean is
the expected type, using the ToBoolean abstract operation. As [] is a truthy value, it is coerced
into true and then the Not operator negates it, converting true into false. So the expression
becomes:
1 [] == false
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
5. The types are equal, so the strict equality comparison is performed, i.e., 0 === 0, giving us true
as an output.
⁷⁵https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_NOT
⁷⁶https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
Coercion 71
:::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.
This is what you should do as well. There is no need to memorize every step. Just understand the
basics of how coercion is performed, which abstract operations are involved, and just refer to the
specification. :::
!!"true" == !!"false"
1. Again, we have two operators in an expression. As mentioned before, the precedence of the
logical Not operator is higher, so the sub-expressions !!"true" and !!"false" will be evaluated
first.
The string "true" in the expression !!"true" is a truthy value, so it will be coerced to the
boolean value true. So the expression will become !!true. Next, we have two occurrences of
the Not operator. Applying it twice to true will first convert it to false and then back to true.
The second sub-expression !!"false" will also evaluate to true because the string "false"
is a truthy value, so same as the first sub-expression, the expression will become !!true and
then applying the Not operator twice will give us true. So after the sub-expressions have been
coerced and evaluated, our expression will become:
1 true == true
2. The types are equal, so the strict equality comparison is performed, i.e., true === true, giving
us true as an output.
[1, 2, 3] + [4, 5, 6]
1. Recall how the addition operator works. The abstract operation involved here is ApplyStringOr-
NumericBinaryOperator⁷⁸.
As both of the operands are objects, they are first converted into primitive values with no
preferred type specified for the ToPrimitive abstract operation. So, by default, “number” is the
preferred type. Arrays, when coerced into primitive values, are converted into primitive values
using the toString method. For the array that we have in our expression, we will get "1,2,3"
and "4,5,6" respectively. So the expression becomes:
1 "1,2,3" + "4,5,6"
2. As both operands, after coercion, are strings, instead of addition, concatenation is performed,
joining both strings, giving us "1,2,34,5,6" as output.
⁷⁷https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal
⁷⁸https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-applystringornumericbinaryoperator
Coercion 72
[undefined] == 0
1. As we have seen many times in this lesson when there is a comparison between an object and a
number using the abstract equality operator, the object is first converted into a primitive value.
Recall from a note earlier in this lesson that [undefined], when converted into a primitive
value, outputs an empty string. So the expression becomes:
1 "" == 0
2. An empty string, when converted into a number, gives us 0. So the expression becomes:
1 0 == 0
3. The types are equal, so the strict equality comparison is performed, i.e., 0 === 0, giving us true
as an output.
[[]] == ''
1. In this expression, we have an array containing an empty array and an empty string. The array
operand is first converted into a primitive value. Recall how arrays are converted into primitive
values. Apart from some corner cases mentioned earlier, arrays are converted into primitives
by coercing their elements into strings and then joining them using commas. So, the nested
empty array will be converted into a primitive value. What do we get when an empty array
is converted into a primitive value? Yes, an empty string. So, the outer array is also converted
into an empty string. The expression after coercion becomes:
1 "" == ""
[] + {}
1. As the operator is an addition operator and both operands are objects, they are both converted
into primitives with no preferred type. So, by default, the preferred type is set to “number”.
An empty array is converted into an empty string, and the default primitive representation of
object literals is the string "[object Object]". So the expression becomes:
2. Both operands are now strings that are concatenated, giving us “‘[object Object]” as an output.
References
Some of the expressions above were taken from the following github repo: wtfjs⁷⁹
⁷⁹https://github.com/denysdovhan/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?
The closure is a combination of the following two things:
• A Function
• A reference to the environment/scope in which that function is created
In other words, whenever we define a function in JavaScript, that function saves a reference to the
environment in which it was created. This is what’s referred to as a closure: a function along with
a reference to the environment in which it is created.
Closures allow a nested function to access the declarations inside the containing function, even after
the execution of the containing function has ended.
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();
1 function learnJavaScript() {
2 function stepsToLearnJavaScript() {
3 console.log(isReading);
4 }
5
6 stepsToLearnJavaScript();
7 }
8
9 learnJavaScript();
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:
scope linkage
⁸⁰https://tc39.es/ecma262/#table-internal-slots-of-ecmascript-function-objects
⁸¹https://stackoverflow.com/questions/33075262/what-is-an-internal-slot-of-an-object-in-javascript
⁸²https://tc39.es/ecma262/#sec-environment-records
Closures 77
Let’s revisit one of the earlier examples from this lesson that involved a nested function:
The linkage between different scopes can be visualized in the image below:
Closures 78
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.
Closures 79
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();
• Many online resources introduce the concept of closures with code examples containing a
function that returns a nested function.
• Closures are only noticeable when a function is invoked from a different scope than the one it
is defined in.
Closures 80
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 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");
1 1;
2 2;
3 3;
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;
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.
scope linkage
Although the callbacks of the setTimeout function log the counter variable, they still have access
to the i variable as well. What if we log both counter and i? How will the output change?
14 ---------
15 */
The output of the code example above can be seen in the Replit below:
<ReplitEmbed src=”https://replit.com/@newlineauthors/closures-in-loops-example3”>
Each callback has a closure over a different instance of the counter variable, but they all still share
the same variable i from the global scope. As a result, they log the latest value of i, i.e., “4”.
ES2015 solution
ES2015 introduced block-scoped variables and constants with the help of let and const keywords,
respectively. We can solve the “closures in loop” problem simply by replacing the var keyword with
the let keyword in our original code example that had this problem.
1. Before the execution of the for loop starts, an environment object (let’s call it initEnv) is
created, and it contains the variables declared in the initialization part of the for loop. In
our case, we have only one variable, i, so the initEnv environment object contains a copy of
variable i. This environment object is linked to the outer environment by saving a reference to
the outer environment in the [[OuterEnv]] internal slot. The outer environment, in this case,
is the global environment.
2. To start the first iteration of the for loop, a new environment object is created (let’s call it
iter1Env). This environment object also gets the global environment as its outer environment.
The variable i is copied into this newly created iter1Env object by copying it from the initEnv
object (created in step 1).
3. The loop condition i <= 3 is true, so the body of the loop is executed as part of the first
iteration of the loop. The callback function for the setTimeout is created, saving a reference to
the environment object of the first loop iteration iter1Env (created in step 2) in the internal
[[Environment]] slot of the callback function. After that, the callback function is passed to the
setTimeout function as an argument to be executed later.
The first iteration has been completed. The following image shows the linkage between
different environments created so far:
Closures 87
The following diagram shows how different environments are linked together after three iterations
of the loop:
Closures 88
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.
With the recent additions to the JavaScript language, it is now possible to have private fields
and methods⁸⁴ in JavaScript. However, before these changes were introduced in the language,
closures were the go-to option for hiding data from public access. In object-oriented programming
terminology, this is referred to as “data hiding” and “encapsulation”.
Following is an example of how we can have private variables and methods using closures:
⁸⁴https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields
Closures 89
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.
Closures 90
Further reading
⁸⁵https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#emulating_private_methods_with_closures
Prototypes
Inheritance is a general object-oriented programming concept allowing objects to inherit other
objects’ methods and properties. This reduces code duplication and promotes code sharing between
different objects.
Unlike traditional object-oriented programming languages like Java or C#, JavaScript has a different
way of dealing with inheritance. Objects in JavaScript are linked to other objects, and this linkage
allows an object to use the functionality of another object to which it is linked.
The linkage between objects in JavaScript forms a chain. This chain is known as the “prototype
chain”. Think of the scope chain, where each scope is linked to another scope until we reach the
global scope. The prototype chain is similar: one object is linked to another object. This other object,
in turn, is linked to another object, forming a chain between objects.
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.
When we create an object literal in JavaScript, it is, by default, linked to the built-in
Object.prototype object.
The Object.prototype object is the prototype of the obj object in the code example above.
The following code example shows that the property named “prototype” exists on functions:
1 // Car.prototype
2 {
3 constructor: <Car function>
4 }
The following code example verifies that Car.prototype.constructor refers to the Car function:
1 Car.prototype.start = function () {
2 console.log("starting the engine of " + this.name);
3 };
4
5 const honda = new Car("honda", "1996");
6 const toyota = new Car("toyota", "2000");
7
8 honda.start(); // starting the engine of honda
9 toyota.start(); // starting the engine of toyota
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.
Some objects are directly linked to the Object.prototype object, while others are linked indi-
rectly. Arrays, for example, are linked indirectly. Each array instance is directly linked to the
Array.prototype object. The Array.prototype object is linked to the Object.prototype object. This
forms a prototype chain that ends at the Object.prototype object.
The Array.prototype object contains the methods that are callable on every array, for example, map,
filter, etc. The Object.prototype object contains the methods that are available to all objects, for
example, the toString method.
Just as JavaScript traverses the scope chain to find the declaration of an identifier that can’t be found
in the current scope, JavaScript traverses the prototype chain to find the properties that cannot be
found in the current object. The prototype chain has to end somewhere. Otherwise, JavaScript will
keep traversing an endless chain; it ends at the Object.prototype object. Accessing the prototype
of Object.prototype returns null.
1 console.log(Object.getPrototypeOf(Object.prototype));
2 // null
“Function” function
As confusing as it may sound, there’s a function named Function⁸⁸. Functions in JavaScript are
objects and are instances of this “Function” constructor function. The Function.prototype object
provides properties that are accessible by all functions; for example, methods like bind, apply, etc.
The Function.prototype object serves as the prototype for functions, including the Object function.
Even the Function function, to which the Function.prototype object belongs, inherits properties
from the Function.prototype object because Function is, after all, just a function itself. So it makes
sense to make it inherit from the Function.prototype object, which contains the common properties
for functions.
1 console.log(Object.getPrototypeOf(Object) == Function.prototype);
2 // true
3
4 console.log(Object.getPrototypeOf(Function) == Function.prototype);
5 // true
As the Object.prototype object is the root or parent object, it is part of the prototype chain, directly
linked to the Function.prototype object.
The __proto__ property is defined on the Object.prototype object. It is a getter and a setter that
returns or sets the prototype of an object. In other words, it returns or sets the value of the internal
Prototypes 99
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 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 };
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
Object.create method
The Object.create method is used to create a new object with another object, passed as the first
argument, as the prototype of the newly created object. This method lets us explicitly set the
prototype of an object. The code example above can be rewritten with Object.create as shown
below:
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.
Consider the following simplified example:
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:
• Person.call(...) is invoked inside the Student constructor function to delegate the responsi-
bility of adding and initializing the name and age properties on the newly created instance or
object of Student.
Prototypes 104
The linkage between the Student.prototype object and the Person.prototype object allows the
instances of Student to use the properties defined on the Person.prototype object.
The image below helps visualize the prototype chain created as a result of our code example:
Prototypes 105
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
As of 2015, JavaScript has classes. They provide a declarative way of writing code that is less
error-prone. Classes come with the extends⁹¹ keyword that helps create a parent-child relationship
between classes. The code example above can be rewritten using classes, as shown below:
1 class Person {
2 constructor(name, age) {
3 this.name = name;
4 this.age = age;
5 }
6
7 introduce() {
8 console.log(`My name is ${this.name} and I am ${this.age} years old`);
9 }
10 }
11
12 class Student extends Person {
13 constructor(name, age, id) {
14 // delegate the responsibility of initializing
15 // "name" and "age" properties to the parent class
16 super(name, age);
17 this.id = id;
18 }
19 }
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:
⁹¹https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends
Prototypes 107
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.
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 this keyword is like an implicit parameter passed to a function. Just like explicit function
parameters, the value of implicit parameter this is set when the function is invoked. This is an
important point. The value of this inside a function depends on how that function is called.
Consider the following code example:
1 const student = {
2 id: 123,
3 name: "John Doe",
4 email: "john@email.com",
5 printInfo: function () {
6 console.log(`${this.id} - ${this.name} - ${this.email}`);
7 }
8 };
9
10 student.printInfo();
11 // 123 - John Doe - john@email.com
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.
What will this refer to if the function is not invoked as a “method”? Consider the following code
example:
1 function orderFood() {
2 console.log("Order confirmed against the name: " + this.fullName);
3 }
4
5 orderFood();
6 // Order confirmed against the name: undefined
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.
1 "use strict";
2
3 function orderFood() {
4 console.log("Order confirmed against the name: " + this.fullName);
5 }
6
7 orderFood();
8 // Uncaught TypeError: this is undefined
<ReplitEmbed src=”https://replit.com/@newlineauthors/what-is-this-example3”>
An error? Why?
Recall what the value of this is inside a “function” in strict mode. It is undefined. So this.fullName
throws an error because we cannot access any properties on the undefined value.
Global context
In the global scope, the value of this depends on the environment in which our JavaScript code is
executed.
JavaScript code can be executed in different environments, for example, browsers, NodeJS, etc. The
value of this in global scope is different in different environments. In the case of browsers, the value
of this in the global scope is the window object.
In NodeJS, the value of this depends on whether we are using the ECMAScript modules or the
CommonJS modules. In ECMAScript modules, the value of this is undefined at the top level of a
module. This is because the code in ECMAScript modules is executed in strict mode. In CommonJS
modules, at the top level of a module, this refers to the module.exports object.
:::info
In Node.js, the JavaScript code is technically not executed in a global scope. Instead, it is executed
in a module scope, where commonly used modules are CommonJS and ECMAScript modules.
:::
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.
⁹³https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
‘this’ keyword 111
The function above, when invoked as a constructor function, will add two properties: name and
ingredients to the newly created object.
Class context
Code inside a class in JavaScript is executed in strict mode. As a result, the value of this inside
methods is either undefined if not invoked on an object or the class instance itself, which is used to
invoke the method.
1 class Shape {
2 constructor(color) {
3 this.color = color;
4 }
5
6 printColor() {
7 console.log(this.color);
8 }
9 }
10
11 const circle = new Shape("Red");
12 const printColorFn = circle.printColor;
13 printColorFn();
14 // Error: this is undefined
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>
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>
Calling the sendRequest method from within the submitForm method throws an error because, as
discussed, this inside an event handler function, submitForm in our case, is an HTML element that
triggered the event. So, unlike what we expected, this.sendRequest throws an error. It would have
worked if this inside the submitForm method was an instance of the FormHandler class. So, how can
we call the sendRequest method from the submitForm method? There are multiple ways to achieve
this, but we will discuss them in the later lessons in this module.
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 };
‘this’ keyword 114
11
12 const counter = new Counter(0);
13 const increment5 = counter.incrementFactory(5);
14 increment5(); // NaN
15 increment5(); // NaN
16 increment5(); // NaN
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
‘this’ keyword 115
17 increment5(); // 10
18 increment5(); // 15
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
counter object. So, this inside the incrementFactory function refers to the counter object, and this
is the surrounding context of the arrow function returned from the incrementFactory function. As
a result, the value of this inside the arrow function, when it is invoked, is also the counter object,
and this is what we wanted this inside the increment5 function to be to make our code example
work.
Let us revisit the example we discussed in the previous lesson:
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>
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.
‘this’ keyword 117
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()⁹⁴
⁹⁴https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
‘this’ keyword 118
• 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
⁹⁵https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
⁹⁶https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
‘this’ keyword 119
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
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"
‘this’ keyword 120
10 };
11
12 // borrow method from john
13 const sayHello = john.sayHello;
14 sayHello.call(sarah);
15 // Hello, I am Sarah
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
‘this’ keyword 121
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.
In other words, the Employee constructor is invoked from inside of the BankEmployee constructor,
with this explicitly set to the newly created BankEmployee object. As a result, properties added
to this inside the Employee constructor will actually add the properties to the newly created
BankEmployee object. This is how we can use the existing constructor function and reduce code
duplication.
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>
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
‘this’ keyword 122
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.
1 if (globalThis.secretProperty) {
2 // execute code
3 }
Although this property is well supported⁹⁷ in modern browsers, older versions of browsers don’t
support it. So, browser support might be taken into consideration when using this property.
“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 ECMAScript modules, code is executed in strict mode. As a result, the value of this in module
scope is undefined, but the value of globalThis is the global object of the execution environment;
in NodeJS, it is the global object, and in the browsers, it is the window object.
This module was all about the this keyword; we discussed different ways its value is set in different
contexts; we also discussed a problem that can arise due to an unexpected value of this and explored
different options we have to fix such problems.
As this can have different values in different contexts, let us summarize what value this has in
different contexts:
• 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.
⁹⁷https://caniuse.com/?search=globalThis
‘this’ keyword 124
– 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.
Hopefully, this module gave you a better understanding of this keyword and helped you understand
how its value is set in different contexts. This module also aims to help you understand the kinds of
problems an unexpected value of this can cause in our code and what different options are available
to us to fix such problems.
Symbol
symbol is a primitive value that can be created using a function named Symbol. What makes this
primitive value interesting is that it is guaranteed to be unique. This guarantee of being unique is
the selling point of symbols.
With the introduction of symbols in ES2015, two things changed in JavaScript:
Before we discuss how to use symbols in our code, let us first understand the motivation behind
adding symbols to the JavaScript language.
Symbols were originally meant to be used as a mechanism to add private properties to objects and
were supposed to be called “private name objects”. But later, their name was changed to symbols,
and they were made a primitive value.
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.
One of the main goals of TC39 is to keep JavaScript backwards compatible. With this goal in mind,
any new feature added to the language must not break existing code. Symbols help keep the promise
of backward compatibility.
Some features in the JavaScript language require looking up a property on an object. What property
keys could have been chosen for such features? Choosing any string property name wasn’t possible
because someone might have used that property in their code, and using that property for a new
feature might have broken their code.
For example, for converting an object into a primitive value, a special property named Sym-
bol.toPrimitive⁹⁸ is looked up on the object by the type conversion algorithm. If such a property
exists and its value is a function, its return value is used as the primitive representation of the object.
Otherwise, the default mechanism of calling the toString and valueOf methods in different order
is used, as explained in an earlier module on coercion.
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?
This is where symbols shine. Using symbols as properties enables such features by adding unique
properties to objects that cannot possibly break existing code because:
⁹⁸https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive
Symbol 126
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.
Once a symbol has been created, it can be added to an existing object as a property using the square
bracket notation:
The description is passed as an argument to the Symbol function. The provided description can then
be accessed using the property named description on symbols.
Symbol.toPrimitive
As explained in an earlier lesson in this module, Symbol.toPrimitive represents a symbol property
that is used by the object to primitive conversion process in JavaScript. Its value is a function that
is passed a hint or the preferred type of the primitive value to represent the object being converted
into a primitive value. The return value is used as the primitive value of the object.
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.
¹⁰¹https://tc39.es/ecma262/#sec-well-known-symbols
Symbol 131
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
The default implementation of the Object.prototype.toString method isn’t very useful for user-
defined objects.
For some built-in objects, the default implementation of the toString method is also not useful. As
a result, many objects override the default toString implementation.
You can see the output of the above code example in this Replit:
<ReplitEmbed src=”https://replit.com/@newlineauthors/well-known-symbols-example3” />
Notice the “Array” part in the output of the default toString implementation. It is known as the tag.
For some built-in objects, the tag is the type of the value, for example, “Array” in the case of arrays
and “String” in the case of strings.
Symbol 132
In the case of user-defined objects, the default toString output, as shown above, is [object Object]
where the tag is “Object” - not very useful.
The well-known symbol Symbol.toStringTag allows us to change the value of the tag.
1 const task = {
2 title: "exercise",
3 isComplete: false,
4 [Symbol.toStringTag]: "Task"
5 };
6
7 console.log(task.toString()); // [object Task]
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
The [Symbole.isConcatSpreadable] property is looked up by the concat method of arrays to
determine if the elements of the array or array-like object passed to the concat method should
be spread or flattened.
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.
¹⁰²https://stackoverflow.com/questions/29707568/javascript-difference-between-array-and-array-like-object
Symbol 133
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¹⁰⁴.
¹⁰³https://tc39.es/ecma262/#sec-well-known-symbols
¹⁰⁴https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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.
Asynchronous JavaScript
Before we dive into how asynchronous code can be written in JavaScript and how it is handled
behind the scenes, let’s take a step back and see the problem we face in JavaScript if we execute
some long-running code, like a loop in the following example:
Asynchronous JavaScript 135
1 function block() {
2 const start = new Date();
3
4 while (new Date() - start < 3000) {
5 // simulate long running operation
6 // that takes approximately 3 seconds
7 }
8 }
9
10 console.log("Before long running operation");
11 // gets logged immediately
12
13 block();
14
15 console.log("After long running operation");
16 // gets logged after approximately 3 seconds
1 <h1>hello world</h1>
2 <button onclick="alert('hello')">Click</button>
Nowadays, JavaScript engines are good enough to execute the code at a very good speed; engines
are designed to heavily optimize the JavaScript code to execute it as efficiently as possible. Still, one
needs to be mindful that the main thread should not be blocked by any code that could take long
enough to make the delay noticeable.
:::info
JavaScript also allows us to execute some code in another thread, independent of the main thread,
using the web workers¹⁰⁷.
:::
Next, let’s discuss the traditional way of writing asynchronous code in JavaScript using callbacks.
We will also discuss the problems with using callbacks.
Using callback functions to handle asynchronous code has been the traditional way of writing
asynchronous code in JavaScript. A callback function is a function that is passed to another function
as an argument and is intended to be invoked after some asynchronous operation. The function that
receives the callback function as an argument typically invokes the callback function with the result
of the asynchronous operation or the error if the asynchronous operation fails.
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.
In simple words, asynchronous operations take place in the background (outside of JavaScript land),
and in the meantime, other things can execute on the main thread (in JavaScript land).
When the asynchronous operation is completed, our JavaScript code is notified, leading to the
execution of the callback function that we provided at the time of initiating the asynchronous
operation.
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.
The following code example shows the use of a callback function by sending an HTTP request to a
fake REST API:
¹⁰⁷https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
Asynchronous JavaScript 137
1 function fetchUser(url) {
2 const xhr = new XMLHttpRequest();
3
4 xhr.addEventListener("load", function () {
5 // check if the operation is complete
6 if (xhr.readyState === 4) {
7 if (xhr.status === 200) {
8 // Request succeeded
9 const data = JSON.parse(xhr.responseText);
10 console.log(data);
11 } else {
12 // Request failed
13 const error = new Error("Failed to fetch todo");
14 console.log(error);
15 }
16 }
17 });
18
19 xhr.open("GET", url);
20 xhr.send();
21 }
22
23 fetchUser("https://jsonplaceholder.typicode.com/todos/1");
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.
Asynchronous JavaScript 138
1 setTimeout(function () {
2 console.log("logged after 2 seconds");
3 }, 2000);
1 setTimeout(function () {
2 console.log("logged after approximately 4 seconds instead of 2");
3 }, 2000);
4
5 const start = new Date();
6
7 // takes approximately 4 seconds to execute
8 while (new Date() - start < 4000) {}
The callback functions are at the heart of asynchronous code in JavaScript. Using callbacks works,
but they also come with problems.
Callback hell
Imagine a scenario where multiple asynchronous operations need to start sequentially because each
operation depends on the result of the previous operation. To handle such a scenario, we have to
nest callbacks, which, depending on the number of asynchronous operations, can lead to code that
is hard to read and maintain. The following code example shows this in action:
1 asyncOperation1((result1) => {
2 asyncOperation2(result1, (result2) => {
3 asyncOperation3(result2, (result3) => {
4 asyncOperation4(result3, (result4) => {
5 // ...more nested callbacks and operations
6 });
7 });
8 });
9 });
This is what’s referred to as the Callback Hell or the Pyramid of Doom because the nesting at each
level creates a structure that looks like a pyramid.
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:
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
Writing error handling code using callbacks, as shown above, is not a pleasant experience. As shown
in the code above, we need to handle errors in each callback, which can lead to duplication of
error handling logic. There is no central place where we can catch and handle errors for all the
asynchronous operations.
In this lesson, we discussed how callbacks are used to write asynchronous code in JavaScript. Though
there are better alternatives like promises and async-await that solve the problems with callbacks
discussed above, callbacks are still commonly used. Although promises solve the problems with
callbacks, they still use callbacks, but in a more manageable way that helps us avoid the callback
hell.
As we know already, the JavaScript language is single-threaded. Long-running code on the main
thread can block the thread; in the case of browsers, blocking the main thread means that browsers
cannot respond to user interactions and cannot render changes on the UI. This is why the screen
freezes when some long-running code blocks the main thread. In the case of NodeJs, in the context
of application servers, blocking the main thread means that the server cannot handle the incoming
HTTP requests until the main thread is unblocked.
To get around the limitation of a single thread where JavaScript code executes, as discussed in the
previous lesson, any asynchronous operation is handled in the background, and in the meantime,
the main thread can do other things.
Asynchronous operations like HTTP requests are handled by the browser in the background, and
when the HTTP request is completed, our JavaScript code is executed using the callback we provided
at the time of starting the HTTP request. The same is true for other asynchronous operations, like
file handling in NodeJS. Every asynchronous operation in NodeJs is either handled by the internal
thread pool of NodeJS or the operating system itself.
If we consider the main thread as the “JavaScript world”, then the asynchronous operations actually
happen outside the JavaScript world. Once the operation is completed, to get back into the JavaScript
world, callbacks are used, which are invoked to execute the JavaScript code in response to the
successful completion or failure of the operation.
Asynchronous JavaScript 141
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.
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
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”.
Asynchronous JavaScript 142
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.
The above steps can be visualized in the following image:
Asynchronous JavaScript 143
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
The following resources are recommended to further our understanding of the event loop:
Promises introduced in ES2015 have transformed the way we handle asynchronous code in
JavaScript. Promises are meant to address the problems we discussed with callbacks.
A promise represents an object that acts as a placeholder for a value that is typically produced as
a result of an asynchronous operation. In other words, a promise object represents the successful
completion or failure of an asynchronous operation. There is a common misconception among
beginners that promises to make our code asynchronous; they do not. Think of promises as a
notification mechanism that notifies us about the success or failure of some operation that is already
asynchronous. Promises wrap asynchronous operations and allow us to execute code when an
asynchronous operation is successfully completed or when it fails. That’s all a promise does. Nothing
more, nothing less. It is only meant to observe the asynchronous operation and notify us when that
operation is completed.
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.
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.
¹⁰⁸http://latentflip.com/loupe
¹⁰⁹https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop
¹¹⁰https://www.youtube.com/watch?v=8aGhZQkoFbQ
¹¹¹https://www.youtube.com/watch?v=cCOL7MC4Pl0
Asynchronous JavaScript 145
Promise states
Promises can be in one of the following three states in their lifecycle:
• 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]]¹¹².
A promise in the pending state is considered unsettled. Once the promise transitions from the
pending state into either a fulfilled or rejected state, it is said to have settled.
• Promise.prototype.then()
• Promise.prototype.catch()
• Promise.prototype.finally()
then method
The then method is used to register a callback that is invoked asynchronously once the promise
is fulfilled, i.e., the asynchronous operation wrapped by the promise completes successfully. This
method allows us to execute code upon the successful completion of an asynchronous operation.
Consider the following code example:
The then method accepts two callback functions as arguments: the fulfillment handler and the
rejection handler. The fulfillment handler is the first argument, as shown in the code example
above. The rejection handler is the optional second argument that is invoked if the promise on
which the then method is called gets rejected.
¹¹²https://262.ecma-international.org/14.0/#table-internal-slots-of-promise-instances
Asynchronous JavaScript 146
The fulfillment handler is passed the result of the asynchronous operation as an argument. In other
words, the fulfillment handler receives the value with which the promise is fulfilled. In the case of an
HTTP request, the promise is fulfilled with the server response, so the fulfillment handler receives
the server response as an argument. On the other hand, the rejection handler receives the rejection
reason as an argument if the promise is rejected.
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.
The catch method is similar to the then method that is called with only a rejection handler, as shown
below:
However, using the catch method to register a rejection handler is more common than using the
second argument of the then method.
Asynchronous JavaScript 147
finally method
Imagine a scenario where we want to send an HTTP request to a server, and while the request is
in progress, we show a loading spinner to the user to indicate that data is being loaded. When the
request completes, either successfully or unsuccessfully, we want to hide the loading spinner. To
achieve this, we will have to duplicate the code that hides the loading spinner in the fulfillment and
rejection handlers, as shown in the following code example:
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:
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:
The Promise constructor takes a callback function as an argument, referred to as the executor
function, that is invoked synchronously to create the promise object. It is common to incorrectly
assume that any code inside the executor function is executed asynchronously, but that is not the
case. The executor function is invoked synchronously to create the promise object. The code inside
the executor function should be any code that starts some asynchronous operation. The newly
created promise object will observe that asynchronous operation. The promise object will notify
us about the success or failure of the asynchronous operation that is initiated inside the executor
function.
How is the asynchronous operation linked to the newly created promise? Through the resolve and
reject functions that, the executor function receives as arguments. The parameters could be given
different names, but it is common practice to name them resolve and reject to clearly indicate
their purpose. The resolve function is used to resolve or fulfill the promise, and the reject function
is used to reject the promise. Let us take a look at a concrete example that will clarify how we can
create a promise object that wraps around an asynchronous operation and is resolved or rejected
depending on whether the asynchronous operation succeeds or fails.
15 // Request failed
16 const error = new Error("Failed to fetch todo");
17 // call the reject function with the rejection
18 // reason or an error as an argument
19 reject(error);
20 }
21 }
22 });
23
24 const url = "https://jsonplaceholder.typicode.com/todos/1";
25 xhr.open("GET", url);
26 xhr.send();
27 });
28
29 // register fulfillment handler
30 p.then((todo) => {
31 console.log(todo);
32 });
33
34 // register rejection handler
35 p.catch((error) => {
36 console.log(error.message);
37 });
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 });
Promise specification
Promises/A+¹¹³ is a standard that defines the behavior of promises in JavaScript. It ensures that
different implementations of promises in different environments conform to the standard behavior
defined in the specification to ensure consistency in the behavior of promises across different
environments.
¹¹³https://promisesaplus.com/
Asynchronous JavaScript 151
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.
To summarize the difference, thenable is an object with a then method, and promise is an object
with a then method that conforms to the Promises/A+¹¹⁴ specification.
In the previous lesson, we discussed the following instance methods of promises:
• Promise.prototype.then()
• Promise.prototype.catch()
• Promise.prototype.finally()
We discussed what each of these methods allows us to do, but what we didn’t discuss is what each
of these methods returns. Their return value is important because it allows for promise chaining,
which is the topic of this lesson.
Each of the instance methods of promises returns a new promise, which enables us to create a chain
of method calls, effectively creating a chain of asynchronous operations. Promise chaining helps
resolve the two main problems we face when using callbacks: “Callback Hell” and error handling.
The following code example shows how we have registered fulfillment and rejection handlers with
the promise returned by the fetch function:
As each promise instance method returns a new promise, we can rewrite the above code as shown
below:
¹¹⁴https://promisesaplus.com/
Asynchronous JavaScript 152
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:
Notice the promises for which fulfillment and rejection handlers have been registered. The fulfill-
ment 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:
Asynchronous JavaScript 153
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
Calling the then method on a promise registers a fulfillment handler on that promise. The then
method itself returns a new promise that is different from the original promise on which the then
method is called.
• What happens to the promise on which the then method is called? In our case, the then method
is called on the pRequest promise.
• What is returned from the fulfillment or rejection handlers passed to the then method? Which
handler will affect the promise returned by the then method depends on which handler is
invoked when the original promise on which the then method is called settles.
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 fulfillment handler is registered and it returns a value that is not a promise or a thenable,
then the promise returned by the then method gets fulfilled with that returned value.
• If the fulfillment handler doesn’t explicitly return any value, the promise returned by the then
method is fulfilled with undefined as the fulfillment value.
Asynchronous JavaScript 155
• If the then method is called on a promise but the fulfillment handler isn’t provided, the promise
returned by the then method gets fulfilled with the same fulfillment value as the original
promise.
• If the fulfillment handler throws any value or an error, the promise returned by the then method
gets rejected with the thrown value as the rejection reason or value.
Asynchronous JavaScript 156
• If the fulfillment handler returns a promise, the promise returned by the then method gets
resolved to the promise returned by the fulfillment handler. One promise getting resolved to
another promise simply means that the fate of one promise (let’s call it p1) depends on the other
promise (let’s call it p2). If p2 is fulfilled, p1 also gets fulfilled with the same fulfillment value.
If p2 gets rejected, p1 also gets rejected with the same rejection value. Promise p1 will wait for
p2 to settle before it also settles and will eventually meet the same fate as p2.
• If only the fulfillment handler is passed to the then method, the promise returned by the then
method also gets rejected with the same rejection reason or value with which the original
promise was rejected.
• If a rejection handler is passed to the then method, then the promise returned by the then
method depends on what happens inside the rejection handler. This works similarly to how it
works in the case of the fulfillment handler:
– If the rejection handler returns a non-promise value, the promise returned by the then
method gets fulfilled with the value returned by the rejection handler.
– If the rejection handler doesn’t explicitly return any value, the promise returned by the
then method is fulfilled with undefined as the fulfillment value.
– If the then method is called on the original promise but the rejection handler isn’t provided,
the promise returned by the then method gets rejected with the same rejection value as
the original promise.
– If the rejection handler throws any value or an error, the promise returned by the then
method gets rejected with the thrown value as the rejection reason or value.
– If the rejection handler returns a promise, the promise returned by the then method gets
resolved to the promise returned by the rejection handler, the same as in the case of the
fulfillment handler discussed earlier.
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.
Asynchronous JavaScript 158
catch promise
The catch method is used to register a rejection handler for a promise in which it is called.
Just like the then method, the catch method also returns a new promise, and just like the then
method promise, the promise returned by the catch method settles depending on the following two
questions:
• If the rejection handler returns a non-promise value, the promise returned by the catch method
gets fulfilled with the value returned by the rejection handler.
Asynchronous JavaScript 159
• 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.
• 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.
Asynchronous JavaScript 160
• If the rejection handler throws any value or an error, the promise returned by the catch method
gets rejected with the thrown value as the rejection reason or value.
• If the rejection handler returns a promise, the promise returned by the catch method gets
resolved to the promise returned by the rejection handler, the same as in the case of the
fulfillment handler discussed earlier.
Asynchronous JavaScript 161
finally promise
The finally method is used to register a callback function that is invoked asynchronously after
the promise settles. The callback passed to the finally method is invoked regardless of whether
the promise on which it is called is fulfilled or rejected. Just like the then and catch methods, the
finally method also returns a new promise, and the settlement of the promise returned by the
finally method depends on the following two questions:
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 });
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:
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 }
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 returns the
string “hello world”, the first then promise will get fulfilled with “hello world” as the fulfillment
value.
As a result, its fulfillment handler is called, which was registered using the second then method
call. The fulfillment handler of the first then promise receives its fulfillment value, i.e., “hello
world”, as an argument. This results in “hello world” getting logged to the console. The console
output so far is shown below:
1 {
2 favouriteLanguage: "JavaScript",
3 name: "John Doe"
4 }
5
6 "hello world"
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
Asynchronous JavaScript 166
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.
4. The promise returned by the catch method is called on the promise returned by the second
then method. As the second then promise is fulfilled, the catch promise also gets fulfilled with
the same fulfillment value as the second then promise. But there is no fulfillment or rejection
handler registered for the catch promise, so its fulfillment is simply ignored. The final output
of the code is shown below:
1 {
2 favouriteLanguage: "JavaScript",
3 name: "John Doe"
4 }
5
6 "hello world"
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:
Asynchronous JavaScript 167
1 {
2 favouriteLanguage: "JavaScript",
3 name: "John Doe"
4 }
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 fulfilled
after approximately two seconds. As soon as it is fulfilled, the promise returned by the then
method will also get fulfilled with the same fulfillment value as the promise returned by its
callback. As a result, its fulfillment handler is called, which was registered using the second
then method call. The fulfillment handler of the first then promise receives its fulfillment value
as an argument, which is logged inside the fulfillment handler. The console output so far is
shown below:
1 {
2 favouriteLanguage: "JavaScript",
3 name: "John Doe"
4 }
5
6 {
7 favouriteLanguage: "JavaScript",
8 name: "John Doe"
9 }
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 implicitly
returns undefined. So the second then promise fulfills with undefined 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.
4. The promise returned by the catch method is called on the promise returned by the second
then method. As the second then promise is fulfilled, the catch promise also gets fulfilled with
the same fulfillment value as the second then promise. But there is no fulfillment or rejection
handler registered for the catch promise, so its fulfillment is simply ignored. The final output
of the code is shown below:
Asynchronous JavaScript 168
1 {
2 favouriteLanguage: "JavaScript",
3 name: "John Doe"
4 }
5
6 {
7 favouriteLanguage: "JavaScript",
8 name: "John Doe"
9 }
Example 3
1 fakeRequest(false)
2 .then((response) => {
3 console.log(response);
4 })
5 .catch((error) => {
6 console.log(error.message);
7 });
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.
Asynchronous JavaScript 169
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 });
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
Asynchronous JavaScript 171
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:
Asynchronous JavaScript 172
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:
Concurrent requests
Imagine a scenario where we want to initiate multiple HTTP requests all at once and wait for their
collective results. We cannot use a promise chain, as shown below, because it will execute each
request in a sequential manner.
12
13 fetch(url1)
14 .then(parseFetchResponse)
15 .then((data1) => {
16 console.log(data1);
17 // initiate second request
18 return fetch(url2);
19 })
20 .then(parseFetchResponse)
21 .then((data2) => {
22 console.log(data2);
23 // initiate third request
24 return fetch(url3);
25 })
26 .then(parseFetchResponse)
27 .then((data3) => {
28 console.log(data3);
29 })
30 .catch((error) => {
31 console.log(error.message);
32 });
¹¹⁶https://en.wikipedia.org/wiki/Concurrent_computing
¹¹⁷https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
Asynchronous JavaScript 174
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.
Using the Promise.race method, we can initiate an HTTP request with a timeout. This method,
just like the Promise.all method, takes an iterable of promises and returns a promise that fulfills
when any of the promises out of one or more promises provided to this method as an input fulfills.
Similarly, the promise returned by this method is rejected when any of the input promises is rejected.
The following code example shows the request timeout implemented using the Promise.race
method:
Asynchronous JavaScript 175
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);
12 console.log(error.message);
13 }
14 }
15
16 const url = "https://jsonplaceholder.typicode.com/todos/1";
17 fetchTodo(url);
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:
• Returning any non-promise value from an async function leads to the fulfillment of the async
function promise, using the returned value as the fulfillment value.
• Not returning any value from the function implicitly returns undefined. This leads to the
function promise getting fulfilled with undefined as the fulfillment value.
• Throwing an error inside the async function rejects the async function promise, using the
thrown value as the rejection reason.
• Returning a promise from the async function results in the async function promise getting
resolved to the promise returned inside the function body. As we learned about one promise
resolving to another promise in one of the earlier lessons in this module, the promise created by
the async function will wait for the promise, returned inside its body, to settle. Eventually, the
async function promise will be fulfilled or rejected depending on what happens to the promise
returned inside the async function.
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:
The await fetch(url) is an expression that will either evaluate the fulfillment value of the promise
returned by the fetch function or it will throw the rejection value if the promise returned by the
fetch function gets rejected. The thrown value can either be caught in the catch block of the
surrounding try-catch block or, if try-catch is not wrapped around the await statement, rejection
of the awaited promise can reject the async function promise, allowing the calling code to handle
the promise rejection.
Asynchronous JavaScript 180
Unlike promise chaining, where we have to register the fulfillment handler to get the fulfillment
value of the promise, the await expressions evaluate the promise fulfillment value, which we can
save in a variable. But how does it work? Isn’t it blocking the main thread while waiting for the
promise to settle?
Whenever an async function is called, it is executed synchronously until the first await expression
is encountered. The function’s execution is suspended or paused until the awaited promise is settled.
Instead of blocking the main thread, the function’s execution is paused, and in the meantime,
the main thread is free to do other things. When the promise is eventually settled, the function’s
execution is resumed, resuming the code execution after the await expression if the promise is
fulfilled or throwing the rejection value of the promise if the awaited promise is rejected.
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:
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:
19 }
20
21 random();
Error handling
To handle promise rejections inside an async function, we can wrap the await expressions with the
try-catch block as shown below:
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:
or using the try-catch block in the calling code if we are using the async await syntax:
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.
Asynchronous JavaScript 187
¹²⁰https://jakearchibald.com/2017/await-vs-return-vs-return-await/
Asynchronous JavaScript 188
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.
¹²¹https://tc39.es/ecma262/#sec-promise-jobs
Asynchronous JavaScript 189
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.
Consider the following code example:
1 console.log("start");
2
3 setTimeout(() => {
4 console.log("setTimeout callback with 500ms delay");
5 }, 500);
6
7 Promise.resolve()
8 .then(() => {
9 console.log("first 'then' callback");
10 })
11 .then(() => {
12 console.log("second 'then' callback");
13 })
14 .then(() => {
15 console.log("third 'then' callback");
16 });
17
18 setTimeout(() => {
19 console.log("setTimeout callback with 0ms delay");
20 }, 0);
21
22 console.log("end");
23
24 /*
25 start
26 end
27 first 'then' callback
28 second 'then' callback
29 third 'then' callback
30 setTimeout callback with 0ms delay
31 setTimeout callback with 500ms delay
32 */
1. A task is created to execute the script, starting the synchronous execution of the code.
2. The first console.log statement is executed, logging “start” on the console.
1 output:
2 -------
3 start
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
4. Moving on with the synchronous execution of the code, Promise.resolve¹²² is called, which
creates a resolved promise. To execute its fulfillment handler, a microtask or job is queued in
the microtask queue.
1 task queue:
2 -----------
3 [task(execute setTimeout callback)]
4
5 microtask queue:
6 ----------------
7 [job(execute fulfillment callback)]
8
9 output:
10 -------
11 start
5. Next, we have a setTimeout call with a 0ms delay. This also schedules a task to execute its
callback.
¹²²https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
Asynchronous JavaScript 191
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
7. As mentioned earlier, microtasks are processed after each task as well as after each callback,
provided that the callstack is empty. The synchronous execution of the code is a task, and
when it ends, the callstack is empty, so any microtasks in the microtask queue are ready to be
processed by the event loop. We have only one microtask in the microtask queue. It will be
processed by logging “first ‘then’ callback” on the console.
Asynchronous JavaScript 192
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
8. The callback function of the first 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 first 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
9. As mentioned earlier, microtasks are processed until the microtask queue is empty, so the newly
queued microtask will also be processed, logging “second ‘then’ callback” on the console.
Asynchronous JavaScript 193
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
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.
Asynchronous JavaScript 194
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
Asynchronous JavaScript 195
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
:::note In this module, we have used the term “resolved” to refer to a promise that is waiting for
another promise to settle. In other words, we have used the term “resolved” to refer to a promise in
a pending state.
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 following are links to some of the Stackoverflow questions that I answered that are related to
microtasks and explain the execution of code examples similar to the one discussed above:
The following is an article that explains the execution of tasks and microtasks with the help of
interactive examples:
In this lesson, we will discuss common promise-related anti-patterns that should be avoided.
Following is a list of anti-patterns we will discuss:
1 function fetchData(url) {
2 return new Promise((resolve, reject) => {
3 fetch(url)
4 .then((res) => res.json(res))
5 .then(resolve)
6 .catch(reject);
7 });
8 }
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 }
¹²⁸https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
Asynchronous JavaScript 197
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.
Unnecessary use of the promise constructor can lead to another problem: if we forget to add the catch
method call to the promise chain inside the Promise constructor, then any error thrown during the
HTTP request won’t be caught. Forgetting to call the reject function inside the executor function
can hide the failure of the asynchronous operation inside the executor function.
1 function fetchData(url) {
2 fetch(url).then((response) => response.json());
3 }
4
5 fetchData("https://jsonplaceholder.typicode.com/todos/1")
6 .then((data) => console.log(data))
7 .catch((error) => console.log(error));
• 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 }
• 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 }
1 function getData(url) {
2 return Promise.reject(new Error()).catch((err) => {
3 console.log("inside catch block in getData function");
4 });
5 }
6
7 getData()
8 .then((data) => console.log("then block"))
9 .catch((error) => console.log("catch block"));
Asynchronous JavaScript 199
We called Promise.reject¹²⁹ inside the getData function, so instead of logging “then block”, why
didn’t “catch block” get logged? Instead of the catch block, why was the callback function of the
then method invoked? Let’s understand how the above code executes:
See this stackoverflow post¹³⁰, which explains this behavior in more detail.
Although the above code is a contrived example, imagine if there was a fetch function call instead
of Promise.reject in the getData function; if the HTTP request is successful, our code will work
without any problem, but if the HTTP request fails, the catch method in the getData function
will convert promise rejection into promise fulfillment. As a result, instead of returning a rejected
promise, the getData function will return a fulfilled promise.
:::info
Sometimes, you do want to convert promise rejection into promise fulfillment to handle the rejection
and let the promise chain continue. This is fine if done intentionally. Just be aware that promise
rejection can turn into promise fulfillment if you are not careful. Doing this will certainly lead to
bugs in your code.
:::
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
¹²⁹https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject
¹³⁰https://stackoverflow.com/questions/62859190/handle-promise-catches-in-typescript
Asynchronous JavaScript 200
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 }
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.
In the above code example, as the executor function is an async function, the error thrown inside it
doesn’t reject the newly-created promise p. As a result, the callback function of the catch method,
called on promise p, never gets called.
If the executor function is a synchronous function, then any error thrown inside the executor
function will automatically reject the newly created promise. Try removing the async keyword in
the above code example and observe the output.
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.
• Iterables
• Iterators
Iterables
Iterable is an object that implements the iterable protocol¹³². According to the iterable protocol,
an object is iterable if it defines the iteration behavior that can be used by the for...of loop to
iterate over the values in the object. The object can implement a method that is referred to by
the property represented by Symbol.iterator¹³³; it is one of the well-known symbols to define the
iteration behavior. The well-known symbols were discussed in the module related to symbols.
In the case of arrays, this method is defined in the Array.prototype object. This method defines the
iteration behavior that is appropriate for arrays. Other objects can also implement this method to
define an iteration behavior that is appropriate for them.
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.
¹³¹https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
¹³²https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol
¹³³https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator
Iterators and Generators 203
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:
• done: indicates whether the iterator can produce or return another. If it can, its value is false,
otherwise, true. The true value is the same as omitting this property in the object returned by
the next method.
• value: the value returned by the iterator object. This property can be omitted or its value can
be undefined if the value of the done property is true.
The following are examples of iterator objects with the above mentioned properties:
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:
14 result = arrayIterator.next();
15 }
16
17 /*
18 2
19 4
20 6
21 8
22 10
23 */
Iterator prototype
Each iterator object inherits from the respective iterator prototype object. For example, the array iter-
ator 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.
¹³⁶https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values
¹³⁷https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys
Iterators and Generators 206
:::info We cannot access the Iterator.prototype object directly because it is a hidden global object
that all built-in iterators inherit from.
:::
The Iterator.prototype object itself is an iterable object, which means that it implements the
iterable protocol. But the Symbol.iterator method that it implements simply returns the iterator on
which this method is called. This means that any iterator object itself is iterable, meaning that we
can use it with constructors like the for...of loop.
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.
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 };
Iterators and Generators 208
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 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 };
Now the studentIterator object is iterable, so we can use it with the for...of loop if we want to.
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 });
Generators are special functions in JavaScript that can suspend their execution at different points
in their execution and then resume from the point at which they were paused. Just like iterators,
generator functions can be used to produce a sequence of values that can be consumed by constructs
like the for...of loop. In fact, a generator function returns a generator object, which is an iterator.
So, we can use the return value of a generator function just like any other iterator.
Following is an example of a generator function that produces odd numbers from 0 to 10:
¹³⁸https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties
¹³⁹https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Iterators and Generators 210
1 function* odds() {
2 for (let i = 1; i <= 10; i += 2) {
3 yield i;
4 }
5 }
6
7 for (const num of odds()) {
8 console.log(num);
9 }
10
11 /*
12 1
13 3
14 5
15 7
16 9
17 */
• The function* syntax marks a function as a generator function. Space between * and function
is also valid syntax: function *.
• The yield keyword produces a value. The value on the right of the yield keyword is what we
get when we call the next method on the iterator object returned from the generator function.
The yield keyword also marks a place where a generator function is paused.
Infinite sequence
Generator functions can also be used to create an infinite sequence. as shown below:
Iterators and Generators 211
1 function* randomNumberGenerator(max) {
2 while (true) {
3 yield Math.floor(Math.random() * max);
4 }
5 }
6
7 const randomNumGen = randomNumberGenerator(10);
8
9 // log 10 random numbers
10 for (let i = 0; i < 10; i++) {
11 console.log(randomNumGen.next().value);
12 }
Implementing iterators
As a generator function returns an iterator object, generator functions make it convenient to write
iterators. We can rewrite the student iterator example in the previous lesson using a generator
function, as shown below:
Iterators and Generators 212
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!
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));
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) {
Iterators and Generators 215
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
Further reading
• Generator (MDN article)¹⁴⁰
• yield* (MDN article)¹⁴¹
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.
JavaScript also has asynchronous iterators that work similarly to synchronous iterators. The
synchronous iterators contain a method named next that, when called, returns an object with two
properties: done and value. The asynchronous iterators also contain a method named next, but
instead of returning an object with the previously mentioned properties, it returns a promise.
Objects implement the Symbol.iterator method to implement the iterable protocol. Objects can
implement the async iterable protocol¹⁴² by implementing the Symbol.asyncIterator¹⁴³ method.
The following is an example of using an async iterator to fetch users from an API:
¹⁴⁰https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator
¹⁴¹https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield*
¹⁴²https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_
protocols
¹⁴³https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator
Iterators and Generators 216
1 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 let userId = 1;
9
10 const userAsyncIterator = {
11 async next() {
12 if (userId > userCount) {
13 return { value: undefined, done: true };
14 }
15
16 const response = await fetch(`${BASE_URL}/${userId++}`);
17
18 if (response.ok) {
19 const userData = await response.json();
20 return { value: userData, done: false };
21 } else {
22 throw new Error("failed to fetch users");
23 }
24 },
25 [Symbol.asyncIterator]() {
26 return this;
27 }
28 };
29
30 return userAsyncIterator;
31 }
32
33 async function getData() {
34 const usersAsyncIterator = fetchUsers(3);
35
36 let userIteratorResult = await usersAsyncIterator.next();
37
38 while (!userIteratorResult.done) {
39 console.log(userIteratorResult.value);
40 userIteratorResult = await usersAsyncIterator.next();
41 }
42 }
43
Iterators and Generators 217
44 getData();
Further reading
• AsyncIterator (MDN article)¹⁴⁴
Similar to the difference between synchronous iterators and async iterators, the main difference
between a regular generator and an async generator is that an async generator yields a promise.
Async generators are a combination of async functions and generators. As a result, we can use both
the await and the yield keywords inside an async generator. Calling an async generator returns an
AsyncGenerator¹⁴⁵ object that implements both async iterator as well as async iterable protocols.
The next method of the AsyncGenerator object returns a promise.
Let us rewrite the async iterator example in the previous lesson to use an async generator:
¹⁴⁴https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator
¹⁴⁵https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator
Iterators and Generators 218
The async generators make it really easy to implement async iterators, and this is how you would
normally implement async iterators.
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
• async function* (MDN article)¹⁴⁷
¹⁴⁶https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of
¹⁴⁷https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*
Debugging JavaScript
Debugging is the process of finding and fixing problems or errors in code. Most software developers,
if not all, spend more time debugging code than writing it. Therefore, debugging is a must-have skill
for any software developer. It is impossible to write bug-free code, so learning to debug your own or
someone else’s code effectively can greatly enhance your productivity as a software developer. We
can’t escape debugging as software developers, so we might as well learn to do it effectively.
In this module, we will learn different ways to debug JavaScript code. Every JavaScript developer
uses the console.log and alert functions to debug their code, and it is fine to do so, but there are
other ways to debug JavaScript. The goal of this module is to introduce the following three ways
that can help us debug JavaScript code effectively:
:::note
The debugging strategies mentioned above depend on the debugger¹⁴⁸ to help us debug our code
effectively.
:::
The next few lessons introduce each of the above-mentioned methods.
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”.
¹⁴⁸https://en.wikipedia.org/wiki/Debugger
Debugging JavaScript 221
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>
:::
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. :::
¹⁵⁰https://balsamiq.com/support/faqs/browserconsole/
Debugging JavaScript 223
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.
Now that we know the problem, we can fix it by converting age to a number before comparing it:
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).
Debugging JavaScript 224
:::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.
:::
Once the code is opened in the browser’s developer tools, setting up a breakpoint is as simple as
clicking on the line number in the code. The following image shows two breakpoints set up in the
code:
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
• Debug JavaScript (chrome devtools docs)¹⁵²
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
¹⁵¹https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer
¹⁵²https://developer.chrome.com/docs/devtools/javascript/
Debugging JavaScript 225
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 }
Before we can run the debugger, we need to setup breakpoints. We can either use the debugger
statement in the code or set breakpoints by clicking on the line number in the JavaScript code file
opened in VS Code. The following image shows a breakpoint added in the JavaScript code file that
is opened in VS Code:
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:
Debugging JavaScript 226
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:
Debugging JavaScript 227
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:
¹⁵³https://code.visualstudio.com/docs/editor/debugging
Wrap up
Congratulation! You have reached the end of this course. Hopefully, this course was able to meet
your expectations, if not exceed them.
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:
• JavaScript reference on MDN¹⁵⁴ is a great resource for referencing different JavaScript topics.
Instead of trying to memorize every JavaScript topic, use MDN to read about it when needed.
• ECMAScript specification¹⁵⁵ is mostly meant for those who implement the specification in the
browsers. Having said that, it can be used to further dive into a specific topic to understand
how it actually works or is supposed to work under the hood.
• exploringjs.com¹⁵⁶ is a great website where you can find books related to JavaScript. The great
thing about this website is that the books are free and can be read online.
In addition to the above-mentioned resources, the following are a few of my favorite books (paid
resources) that can serve as great resources for 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.
¹⁵⁴https://developer.mozilla.org/en-US/docs/Web/JavaScript
¹⁵⁵https://www.ecma-international.org/publications-and-standards/standards/ecma-262/
¹⁵⁶https://exploringjs.com/