Capture of Certificates.dev website

Preparing for Mid-Level JavaScript Certification

It’s crazy how much JavaScript can do. And I’m not at the expert level yet!

JavaScript is the programming language of the web browser and can also be found on web servers, IoT devices, and more.

You can learn about on those two websites:

Both have code examples, exercises and information you should know or be aware of.

All the following is what I’ve found important to take notes of while preparing for the Certificates.dev JavaScript Mid-level certification.

Let’s dive in!

JavaScript Fundamentals

Intro to JavaScript

For this exam, Certificates.dev expect that you should be familiar with both the basics of JavaScript and more intermediate concepts. This chapter gives a refresher of the basics that you review in their Junior Level Certification.

Basic Debugging

During the certification exam, you should know simple issues you might face writing JavaScript and how you can fix those issues using the console and console.log.

Variables and Data Types

Variables are like containers where you can store and label different types of information. That information can be a string of characters like those that makeup your name or a number that can be used in Math equations, like your age, or a variety of other types.

During the exam you’ll need to know the various ways of declaring variables with let and const. You’ll also need to be familiar with primitive data types like strings, numbers, and booleans. Later on we’ll also discuss more complex types, like arrays and objects.

My notes were the following:

  • Using string[index] , you can access the character at the index, or position in the string, if it’s a valid index.
  • Variables named apple and APPLE are two different variables.
  • Even if without use strict we can create a variable without an assignment, it’s strongly recommended not to!
  • Hoisting is declaring a variable anywhere in the code and this is equivalent to declaring it at the top. It’s performed before any execution occurs.
  • However, var declarations are only hoisted to the top of the current script. If you have two <script> elements within one HTML, the first script can’t access variables declared by the second before the second script has been processed and executed.
  • The meaning of undefined is “value isn’t assigned”.

Basic Operators

For the exam, you should know how to use operators to perform match operations and comparisons.

For example, when comparing string, remember that:

  1. it compares unicode values so the unicode value of a (U+0061) is lower than ä (U+00E4).
  2. it compares the characters at the same index in both string, one by one until the end of the shorter string is reached or both strings.

The binary + always concatenates the operands if one operand is a string.

The unary + converts any other type to the number equivalent. Remember: unary takes precedence to binary operation.

The binary - convert the operands to numbers. Same with > or < or /or *.

⚠️ Warning

No conversion though with ==.

Number conversion of undefined or any alphabetical only string produces aNaN .

Number conversion makesnull become a 0 .

Be careful:

1
2
alert(null === undefined); // false
alert(null == undefined); // true

Don’t use comparisons >=, >, < or <= with a variable which may be null/undefined, unless you’re really sure of what you’re doing.

The prefix form (for ex: ++counter) increments counter and returns the new value. However the postfix form (for ex: counter++) increments counter and returns the old value.

Arrays

Arrays allow us to store lists of data. For example, a list of products to display on an e-commerce site or a list of news articles to browse through.

For the exam, you should know the basics of working with arrays; things like: getting array length, adding new items to arrays, accessing an item at a specific index, removing items from arrays, and more.

For this exam, you’ll also need to know advanced array methods but we’ll cover that in the Advanced Array and Object Methods chapter.

For example, someArray.at(-1) is equivalent to someArray[someArray.length - 1].

But you can’t go beyond the array length negatively as you can go positively. Otherwise, you get a undefined.

Methods push/pop run fast, while shift/unshift are slow.

Use for...of on arrays if you don’t need the index.

⚠️ BUT...

⚠️ DON’T USE for...in on arrays!

⚠️ DON’T ASSIGN property length of an array… or suffer the consequences!

⚠️ About (0 == []) is equivalent to (0 == ''), that itself is equivalent to (0 == 0) that finally is true.

Control Structures

Conditionals are how we give our code the ability to make decisions based on the values of our variables. Loops are useful for doing the same thing over and over again.

For example, the precedence of AND (&&) operator is higher than OR (||).

The do...while syntax should only be used when you want the body of the loop to execute at least once regardless of the condition being truthy. Otherwise, the other form is preferred: while(…) {…}.

Understand the for loops below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for (; i < 3; i++) {
  // no need for "begin"
  alert(i); // 0, 1, 2
}

for (; i < 3; ) {
  alert(i++);
}

for (;;) {
  // repeats without limits
}

You can label imbricated loops to beak/continue the outer one:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
outer: for (let i = 0; i < 3; i++) {
  for (let j = 0; j < 3; j++) {
    let input = prompt(`Value at coords (${i},${j})`, "");

    // if an empty string or canceled, then break out of both loops
    if (!input) break outer; // whereas a simple `break` stop the inner loop

    // do something with the value...
  }
}

Any expression can be a switch/case argument:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let a = "1";
let b = 0;

switch (+a) {
  case b + 1:
    alert("this runs, because +a is 1, exactly equals b+1");
    break;

  default:
    alert("this doesn't run");
}

Also, equality check is always strict on a switch.

Functions

Functions allow you to store a piece of code that does a single task inside a defined block. Then call that code whenever you need it using a single short command — rather than having to type out the same code multiple times.

Learn the various syntax for defining functions, how to call functions, and how to make them flexible with arguments.

If a same-named variable is declared inside the function then it shadows the outer one. For instance, in the code below the function uses the local userName. The outer one is ignored:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let userName = "John";

function showMessage() {
  let userName = "Bob"; // declare a local variable

  let message = "Hello, " + userName; // Bob
  alert(message);
}

// the function will create and use its own userName
showMessage();

alert(userName); // John, unchanged, the function did not access the outer variable

Minimize the use of global variables.

A parameter is the variable listed inside the parentheses in the function declaration (it’s a declaration time term).

An argument is the value that is passed to the function when it’s called (it’s a call time term).

A function returns undefined by default if no return or an empty return is used.

⚠️ A function should do exactly what is suggested by its name, no more.

A Function Expression is created when the execution reaches it and is usable only from that moment. So it can’t be called before.

Contrary, a Function Declaration can be called earlier than it’s defined. However, in strict mode, when a Function Declaration is within a code block, it’s visible everywhere inside that block, but not outside of it.

In arrow functions, this is undefined! Arrow functions don’t have their own this. Consequently, they don’t have the special arguments object either.

Below, arguments corresponds to the sayHi method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function defer(f, ms) {
  return function () {
    // arguments = string "John" = parameter "who" of `sayHi`
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(who) {
  alert("Hello, " + who);
}

let sayHiDeferred = defer(sayHi, 2000);
sayHiDeferred("John"); // Hello, John after 2 seconds

Without arrow function, we’d write the following code:

1
2
3
4
5
6
7
8
function defer(f, ms) {
  return function (...args) {
    let ctx = this;
    setTimeout(function () {
      return f.apply(ctx, args);
    }, ms);
  };
}

Objects

Objects are data structures used for grouping variables (properties) together with related functions (methods).

I talk about this in my notes on the Junior level certification.

DOM Manipulation

What is the DOM and Traversing It

The Document Object Model, or DOM for short, represents all page content as objects that can be modified. The document object is the main “entry point” to the page. We can change or create anything on the page using it. For the exam, know the basics of what the DOM is and how to interact with it.

So DOM = Document Object Model and BOM = Browser Object Model.

If we put something after </body>, then that is automatically moved inside the body, at the end, as the HTML spec requires that all content must be inside <body>.

Browsers automatically process errors in the document: close tags, add missing ones (tbody) and so on. But in my experience, it doesn’t solve everything. The best is to use a formatter like Prettier in your IDE to know, as you code, when the syntax is incorrect.

In particular, if a script is inside <head>, then document.body is unavailable, because the browser didn’t read it yet. That’s why scripts are usually put at the bottom of the body tag.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<html>
  <body>
    <div>Begin</div>
    <ul>
      <li>Information</li>
    </ul>
    <div>End</div>
    <script>
      for (let i = 0; i < document.body.childNodes.length; i++) {
        alert(document.body.childNodes[i]); // Text, DIV, Text, UL, ..., SCRIPT
      }
    </script>
    <!-- ...more stuff... -->
  </body>
</html>

If we run the example above, the last element shown is <script>. In fact, the document has more stuff below, but at the moment of the script execution the browser didn’t read it yet, so the script doesn’t see it.

childNodes looks like an array. But actually it’s not an array, but rather a collection. You can use for...of on it since the Symbol.iteratoris set.

⚠️ DON’T USE for...in on DOM collections.

But you can’t use array methods on DOM collection. To be able to, cast the collection using Array.from to transform the collection into an array.

⚠️ DOM collections are read-only.

The parentElement property returns the parent “element”, while parentNode returns “any node” that is the parent. These properties are usually the same: they both get the parent.

With the one exception of document.documentElement:

1
2
alert(document.documentElement.parentNode); // document
alert(document.documentElement.parentElement); // null

Targeting and Manipulating Elements

With JavaScript, it’s easy to query the DOM for a specific element.

Searching: getElement*, querySelector*

⚠️ Please don’t use id-named global variables to access elements.

1
2
3
4
5
6
7
<div id="elem"></div>

<script>
  let elem = 5; // now elem is 5, not a reference to <div id="elem">

  alert(elem); // 5
</script>

The method getElementById can be called only on document object. It looks for the given id in the whole document.

Check out the interesting closest method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<h1>Contents</h1>
<div class="contents">
  <ul class="book">
    <li class="chapter">Chapter 1</li>
    <li class="chapter">Chapter 2</li>
  </ul>
</div>
<script>
  let chapter = document.querySelector('.chapter'); // LI
  alert(chapter.closest('.book')); // UL
  alert(chapter.closest('.contents')); // DIV
  alert(chapter.closest('h1')); // null (because h1 is not an ancestor)
</script>

All methods "getElementsBy*" (ByTag, ByName, etc.) return a live collection. Such collections always reflect the current state of the document and “auto-update” when it changes.

In contrast, querySelectorAll returns a static collection.

Method Searches by… Can call on an element? Live?
querySelector CSS-selector
querySelectorAll CSS-selector
getElementById id -
getElementsByName name -
getElementsByTagName tag or '*'
getElementsByClassName class

Nodes properties

The nodeType property provides one more, “old-fashioned” way to get the “type” of a DOM node. However, in modern scripts, we sohuld use instanceof.

innerHTML works on elements objects, not node.

“innerHTML+=” does a full overwrite

⚠️ Therefore, as the content is “zeroed-out” and rewritten from scratch, all images and other resources will be reloaded.

About outerHTML :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<div>Hello, world!</div>

<script>
  let div = document.querySelector("div");
  // replace div.outerHTML with <p>...</p>
  div.outerHTML = "<p>A new element</p>"; // (*)
  // Wow! 'div' is still the same!
  alert(div.outerHTML); // <div>Hello, world!</div> (**)
  p = document.querySelector("p");
  alert(p.outerHTML);
</script>

So what happened in div.outerHTML=... is:

  • div was removed from the document.
  • Another piece of HTML <p>A new element</p> was inserted in its place.
  • div still has its old value. The new HTML wasn’t saved to any variable.

Attributes and properties

The browser parses one-to-one known HTML attributes to DOM properties on an element basis (since they aren’t the same always).

The non-standard attributes aren’t parsed.

All attributes are accessible by using the following methods:

  • elem.attributes: get all attributes.
  • elem.hasAttribute(name): checks for existence.
  • elem.getAttribute(name): gets the value.
  • elem.setAttribute(name, value): sets the value.
  • elem.removeAttribute(name): removes the attribute.

Attribute names are case-insensitive.

Property-attribute synchronization is automatic, but not for input ’s value.

Why would using an attribute be preferable to having classes like .order-state-new.order-state-pending.order-state-canceled?

Because an attribute is more convenient to manage. The state can be changed as easy as:

1
2
// a bit simpler than removing old/adding a new class
div.setAttribute("order-state", "canceled");

All attributes starting with “data-” are reserved for programmers’ use. They’re available in the dataset property. Multiword attributes like data-order-state become camel-cased: dataset.orderState.

Modifying the Document

  • node.append(...nodes or strings): append nodes or strings at the end of node,
  • node.prepend(...nodes or strings): insert nodes or strings at the beginning of node,
  • node.before(...nodes or strings): insert nodes or strings before node,
  • node.after(...nodes or strings): insert nodes or strings after node,
  • node.replaceWith(...nodes or strings): replaces node with the given nodes or strings.

Strings are inserted in a safe way:

1
2
3
4
<div id="div"></div>
<script>
  div.before("<p>Hello</p>", document.createElement("hr"));
</script>

outputs:

1
2
3
&lt;p&gt;Hello&lt;/p&gt;
<hr />
<div id="div"></div>

Other methods: insertAdjacentText and insertAdjacentElement that work as follows.

The call elem.cloneNode(true) creates a “deep” clone of the element – with all attributes and subelements. If we call elem.cloneNode(false), then the clone is made without child elements.

The call to document.write only works while the page is loading. If we call it afterwards, the existing document content is erased.

Styles and Classes

Methods of classList:

  • elem.classList.add/remove("class"): adds/removes the class.
  • elem.classList.toggle("class"): adds the class if it doesn’t exist, otherwise removes it.
  • elem.classList.contains("class"): checks for the given class, returns true/false.

Browser-prefixed properties like -moz-border-radius-webkit-border-radius follow this rule: a dash means an upper case. Otherwise, for a multi-word property, camelCase is used.

There is a special method for that, elem.style.removeProperty('style property').

To set the full-style like div.style="color: red; width: 100px", use the special property style.cssText.

⚠️ Don’t forget to add CSS units to CSS property values.

The style property operates only on the value of the "style" attribute, without any CSS cascade.

To get the style of CSS classes, you need to use getComputedStyle(element, [pseudo]).

⚠️ getComputedStyle requires the full property name

We should always ask for the exact property that we want, like paddingLeft or marginTop or borderTopWidth. Otherwise the correct result isn’t guaranteed.

For instance, if there are properties paddingLeft/paddingTop, then what should we get for getComputedStyle(elem).padding? Nothing, or maybe a “generated” value from known paddings? There’s no standard rule here in browser implementations.

Styles applied to :visited links are hidden! Why? JavaScript may not see the styles applied by :visited. And also, there’s a limitation in CSS that forbids applying geometry-changing styles in :visited. That’s to guarantee that there’s no way for an evil page to test if a link was visited and hence to break the privacy.

Events

Event Handling

Reacting to user events such as clicking, scrolling, moving the mouse, and more is an essential skill to master when using JavaScript in the browser.

As we know, HTML attribute names aren’t case-sensitive, so ONCLICK works as well as onClick and onCLICK… But usually attributes are lowercased: onclick.

However, DOM properties are case-sensitive.

As there’s only one onclick property, we can’t assign more than one event handler. Otherwise, the JavaScript handler overwrites the in-HTML handler.

The value of this inside a handler is the element targeted.

Be careful:

1
2
3
4
5
// right
button.onclick = sayThanks;

// wrong: If we add parentheses, then sayThanks() becomes a function call
button.onclick = sayThanks();

Don’t use setAttribute for handlers.

The syntax to add a handler: element.addEventListener(event, handler, [options]);.

  • event is the event name, e.g. "click".**handler
  • handler is the function that handles the event.
  • options is an additional optional object with properties like:
    • once: if true, then the listener is automatically removed after it triggers.
    • capture: the phase where to handle the event, to be covered later in the chapter Bubbling and capturing. For historical reasons, options can also be false/true, that’s the same as {capture: false/true}.
    • passive: if true, then the handler won’t call preventDefault(), we’ll explain that later in Browser default actions.

To remove a handler we should pass exactly the same function as was assigned.

So this doesn’t work:

elem.addEventListener( "click" , () => alert('Thanks!'));// .... elem.removeEventListener( "click", () => alert('Thanks!'));

The handler won’t be removed, because removeEventListener gets another function – with the same code, but that doesn’t matter, as it’s a different function object.

Here’s the right way:

`function handler() {alert( ‘Thanks!’ );}

input.addEventListener(“click”, handler);// …. input.removeEventListener(“click”, handler);`

Please note – if we don’t store the function in a variable, then we can’t remove it. There’s no way to “read back” handlers assigned by addEventListener.

For some events, handlers only work with addEventListener, for example, the DOMContentLoaded event.

We can assign not just a function, but an object as an event handler using addEventListener. When an event occurs, its handleEvent method is called.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<button id="elem">Click me</button>

<script>
  let obj = {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    },
  };

  elem.addEventListener("click", obj);
</script>

We could also use objects of a custom class.

The method handleEvent doesn’t have to do all the job by itself. It can call other event-specific methods instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Mouse button pressed";
    }

    onMouseup() {
      elem.innerHTML += "...and released.";
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

Event handlers can also be removed by passing an AbortSignal to addEventListener() and then later calling abort() on the controller owning the AbortSignal. For example, to add an event handler that we can remove with an AbortSignal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = "on" + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Mouse button pressed";
    }

    onMouseup() {
      elem.innerHTML += "...and released.";
    }
  }

  let menu = new Menu();
  elem.addEventListener("mousedown", menu);
  elem.addEventListener("mouseup", menu);
</script>

About removing handlers, beside using removeEventListener, you can use this method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const controller = new AbortController();

btn.addEventListener(
  "click",
  () => {
    const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
    document.body.style.backgroundColor = rndCol;
  },
  { signal: controller.signal }, // pass an AbortSignal to this handler
);

Then the event handler created by the code above can be removed like this:

1
controller.abort(); // removes any/all event handlers associated with this controller

Event Bubbling

Webpages are made up of nested elements, meaning that a JavaScript must support event bubbling. You should be familiar with what event bubbling is, how to work with methods like stopPropagation, and how to utilize it for event delegation.

Reacting to user events such as clicking, scrolling, moving the mouse, and more is an essential skill to master when using JavaScript in the browser.

Bubbling and Capturing

When an event happens on an element, it first runs the handlers on it, then on its parent, then all the way up on other ancestors.

Source: https://javascript.info/bubbling-and-capturing

Almost all events bubble, except for a focus event.

The method stopPropagation() stops the propagation, but only on the handler using it. On other handlers for the same elements, propagation will occur. To stop propagation on all handler, use event.stopImmediatePropagation() .

Don’t stop bubbling without a need!

⚠️ Instead of preventing bubbling, using custom events can help without event.stopPropagation() hidden pitfalls.

Source: https://javascript.info/bubbling-and-capturing

To catch an event on the capturing phase, we need to set the handler capture option to true.

1
2
3
4
elem.addEventListener(..., {capture: true})

// or, just "true" is an alias to {capture: true}
elem.addEventListener(..., true)

⚠️ To remove the handler, removeEventListener needs the same phase. And the event.stopPropagation() during the capturing also prevents the bubbling.

Event Delegation

The idea is that if we have a lot of elements handled in a similar way, then instead of assigning a handler to each of them – we put a single handler on their common ancestor.

⚠️ For document-level handlers – always addEventListener

We can also use event delegation to add “behaviors” to elements declaratively, with special attributes and classes. For ex:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<button data-toggle-id="subscribe-mail">Show the subscription form</button>

<form id="subscribe-mail" hidden>Your mail: <input type="email" /></form>

<script>
  document.addEventListener("click", function (event) {
    let id = event.target.dataset.toggleId;
    if (!id) return;

    let elem = document.getElementById(id);

    elem.hidden = !elem.hidden;
  });
</script>

Common UI Events

There are a plenty of user triggered events that JavaScript developers can listen for. You should be familiar with some of the most commons ones: mouse events, scroll, pointer events, and keyboard events:

Mouse Events

On mouse event, you can use these properties:

  • shiftKey: Shift
  • altKey:  Alt (or Opt for Mac)
  • ctrlKey: Ctrl
  • metaKey:  Cmd for Mac

About metaKey : on Mac it’s usually Cmd instead of Ctrl so where a Windows user presses Ctrl+Enter or Ctrl+A, a Mac user would press Cmd+Enter or Cmd+A, and so on. So for JS-code it means that we should check if (event.ctrlKey || event.metaKey).

But there are also mobile devices, so if their device doesn’t a keyboard, then there should be a way to live without modifier keys.

Using metakey with events:

1
2
3
4
5
button.onclick = function (event) {
  if (event.altKey && event.shiftKey) {
    alert("Hooray!");
  }
};

Now, you should know about the double-click caveat:

1
2
3
4
Before...
<!-- prevent text selection in "Double-click me" with onmousedown's handler -->
<b ondblclick="alert('Click!')" onmousedown="return false"> Double-click me </b>
...After

If we want to disable selection to protect our page content from copy-pasting, then we can use another event: oncopy.

Moving the mouse

Interesting events mouseover/mouseout, relatedTarget : read more at Events mouseover/mouseout, relatedTarget.

I’m sure not to grasp the Mouseout when leaving for a child scenario. Maybe read it again.

It seems that there is an extra mouseover.

Compared to mouseover/mouseout, there are two important differences with mouseenter/mouseleave:

  1. Transitions inside the element, to/from descendants, aren’t counted.
  2. Events mouseenter/mouseleave don’t bubble. So we can’t use event delegation with them.

⚠️ I feel there’ll be something about this in the code challenge of the exam.

Drag’n’Drop with mouse events

The browser has its own drag’n’drop support for images and some other elements. It runs automatically and can conflict with any customization.

To disable it:

1
2
3
ball.ondragstart = function () {
  return false;
};

⚠️ Also, be aware of the positioning: see the example > Correct positioning

Check out the example of a dragged element over droppable element: Potential drop targets (droppables)

Pointer events

Unless you develop for old browsers, such as Internet Explorer 10, or for Safari 12 or below, there’s no point in using mouse or touch events any more – we can switch to pointer events.

Pointer event Similar mouse event
pointerdown mousedown
pointerup mouseup
pointermove mousemove
pointerover mouseover
pointerout mouseout
pointerenter mouseenter
pointerleave mouseleave
pointercancel -
gotpointercapture -
lostpointercapture -

We can replace mouse<event> events with pointer<event> in our code and expect things to continue working fine with mouse.

The support for touch devices will also “magically” improve. Although, we may need to add touch-action: none in some places in the CSS.

Keyboard: keydown and keyup

The key property of the event object allows to get the character, while the code property of the event object allows to get the “physical key code”.

Case matters: "KeyZ", not "keyZ" ?

To reliably track layout-dependent characters, event.key may be a better way.

On the other hand, event.code has the benefit of staying always the same, bound to the physical key location. So hotkeys that rely on it work well even in case of a language switch.

Do we want to handle layout-dependant keys? Then event.key is the way to go.

We want a hotkey to work even after a language switch? Then event.code may be better.

Which to use: key or code property?

For events triggered by auto-repeat, the event object has event.repeat property set to true.

⚠️ On form inputs, be aware of the Enter key behavior that submits the form if the focus is inside any input. To cancel this, use keydown, not keyup event to cancel browser default behavior.

Scrolling

This article summarizes the properties to use to calculate position in the windows when the scroll, scrollendor scrollstarthappen.

Advanced Functions

This is the part I have the most difficulty to grasp…

But there is a very good code challenge if you purchase the preparation bundle. So I recommend that you get it if you struggle with the following concepts. To me, an example is always better than the theory.

First Class and Higher Order Functions

Functions in JavaScript are treated much like any other variable. This means they can be passed around as the argument to other functions or even returned from functions. You need to learn how to take advantage of the first-class nature of functions to create higher-order functions.

First-class Function Definition

A programming language is said to have first-class functions when functions in that language are treated like any other variable.

We assigned an Anonymous Function in a variable, then we used that variable to invoke the function by adding parentheses () at the end.

Higher-Order Functions

A function that returns a function or takes other functions as arguments is called a higher-order function.

The map or reduce functions of the Array type are examples of that.

Practical example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// logic to clculate area
const area = function (radius) {
  return Math.PI * radius * radius;
};

// logic to calculate diameter
const diameter = function (radius) {
  return 2 * radius;
};

// a reusable function to calculate area, diameter, etc
const calculate = function (radius, logic) {
  const output = [];
  for (let i = 0; i < radius.length; i++) {
    output.push(logic(radius[i]));
  }
  return output;
};

console.log(calculate(radius, area));
console.log(calculate(radius, diameter));

Higher-order functions can help improve the legibility of your code by making it more concise and easy to understand. This can help speed up the development process and make it easier to debug code.

Second, higher-order functions can help organize your code into smaller chunks, making it easier to maintain and extend.

Callbacks

Examples of synchronous callbacks include the callbacks passed to Array.prototype.map()Array.prototype.forEach(), etc. Examples of asynchronous callbacks include the callbacks passed to setTimeout() and Promise.prototype.then().

Is Callback a higher-order function? Or not always?

A callback isn’t inherently a higher-order function, but the two concepts are closely related in JavaScript.

  • A callback is a function passed as an argument to another function, to be invoked later (either synchronously or asynchronously). For example, in array.map(x => x * 2), the arrow function is a callback.

  • A higher-order function is any function that takes one or more functions as arguments, or returns a function as its result. For example, map, filter, and setTimeout are higher-order functions because they expect a function as a parameter.

Other Advanced Function Concepts

Functions and closures are closely coupled concepts that any advanced JavaScript dev should be familiar with. Immediately invoked function expression (or IIFEs) are also a useful tool for the advanced dev. For the exam, be familiar with both of these concepts.

Closure

closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives a function access to its outer scope.

In JavaScript, closures are created every time a function is created, at function creation time.

1
2
3
4
5
6
7
8
9
function init() {
  const name = "Mozilla"; // name is a local variable created by init
  function displayName() {
    // displayName() is the inner function, that forms a closure
    console.log(name); // use variable declared in the parent function is accessible from `displayName`
  }
  displayName();
}
init();

IIFE

IIFE = Immediately Invoked Function Expression

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// standard IIFE
(function () {
  // statements…
})();

// arrow function variant
(() => {
  // statements…
})();

// async IIFE
(async () => {
  // statements…
})();
// With argument
// Logs "testing IFFE"
((myVar) => console.log(myVar))("testing IFFE");

Use cases of IIFEs include:

  • Avoiding polluting the global namespace by creating a new scope, which can be useful with setInterval or setTimeout used inside a for loop where you’d need access to the index of the loop.
  • Creating a new async context to use await in a non-async context.
  • Computing values with complex logic, such as using multiple statements as a single expression.

Method Chaining

Method chaining allow executing on a same instance many methods in a chain matter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
let firstName = "  Rajat  ";
console.log(firstName);
let modifiedName = firstName.toUpperCase().trim();
console.log(modifiedName);

//creating a function Land()
function Land() {
  this.area = "";
  this.status = "for Sale";
}
//Setting status open for sale
Land.prototype.open = function () {
  this.status = "Open for Sale";
  return this;
};
//Setting status not for sale
Land.prototype.close = function () {
  this.status = "Not for Sale";
  return this;
};
//Setting Parameters
Land.prototype.setParams = function (area) {
  this.area = area;
  return this;
};
//printing land status
Land.prototype.doorStatus = function () {
  console.log("The", this.area, "Land is", this.status);
  return this;
};

//creating a land object
let land = new Land();
land.setParams("500 sq ft").close().doorStatus().open().doorStatus();

Asynchronous JavaScript and API’s

Promises and Async/Await

Many operations in JavaScript are done asynchronously. Promises and async/await are the go-to way of working with such async code.

Callback Asynchronous Programming

Here is a “callback-based” style of asynchronous programming:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function loadScript(src, callback) {
  let script = document.createElement("script");
  script.src = src;

  // handle nominal scenario
  script.onload = () => callback(script);
  // handle errors
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

loadScript("/my/script.js", function (error, script) {
  if (error) {
    // the “error-first callback” style
    // handle error
    return;
  }

  // script loaded successfully
  newFunction(); // function from script.js
});

Promises

It’s either one resolve or one reject.

It’s recommended to reject with Error objects.

The properties state and result of the Promise object are internal. We can’t directly access them. We can use the methods .then, .catch and .finally for that. They’re described below:

  • .then follow the last resolve

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    let promise = new Promise((resolve) => {
      setTimeout(() => resolve("done!"), 1000);
    });
    
    promise.then(alert); // shows "done!" after 1 second
    // equivalent to resolve the first function in .then
    promise.then(
      (result) => alert(result), // shows "done!" after 1 second
      null, // doesn't run
    );
    
  • .catch follow the last reject

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    let promise = new Promise(function (resolve, reject) {
      setTimeout(() => reject(new Error("Whoops!")), 1000);
    });
    
    promise.catch(alert); // shows "Error: Whoops!" after 1 second
    // equivalent to reject runs the second function in .then
    promise.then(
      (result) => alert(result), // doesn't run
      (error) => alert(error), // shows "Error: Whoops!" after 1 second
    );
    
  • .finally follow catch (or then if you don’t use the short hand). It doesn’t get the outcome of the previous handler (it has no arguments).

We can conclude that then and finally are always called.

We can implement Thenable object which isn’t a promise:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // resolve with this.num*2 after the 1 second
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise((resolve) => resolve(1))
  .then((result) => {
    return new Thenable(result); // (*)
  })
  .then(alert); // shows 2 after 1000ms

Promises Chaining

Performing a return inside a then allows chaining to another then. The next then gets the previous thenresult.

Chaining after a timeout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
fetch("/article/promise-chaining/user.json")
  .then((response) => response.json())
  .then((user) => fetch(`https://api.github.com/users/${user.name}`))
  .then((response) => response.json())
  .then(
    (githubUser) =>
      new Promise(function (resolve, reject) {
        // (*)
        let img = document.createElement("img");
        img.src = githubUser.avatar_url;
        img.className = "promise-avatar-example";
        document.body.append(img);

        setTimeout(() => {
          img.remove();
          resolve(githubUser); // (**)
        }, 3000);
      }),
  )
  // (**) => then below triggers after 3 seconds, once the `resolve`
  // is called inside the timeout
  .then((githubUser) => alert(`Finished showing ${githubUser.name}`));

Below works the same but with step-by-step functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function loadJson(url) {
  // step 1
  return fetch(url).then((response) => response.json());
}

function loadGithubUser(name) {
  //step 2
  return loadJson(`https://api.github.com/users/${name}`);
}

function showAvatar(githubUser) {
  // step 3
  return new Promise(function (resolve, reject) {
    let img = document.createElement("img");
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  });
}

// Use them:
loadJson("/article/promise-chaining/user.json")
  .then((user) => loadGithubUser(user.name))
  .then(showAvatar)
  .then((githubUser) => alert(`Finished showing ${githubUser.name}`));

As a good practice, an asynchronous action should always return a promise. That makes it possible to plan actions after it; even if we don’t plan to extend the chain now, we may need it later.

Error Handling With Promises

The easiest way to catch all errors is to append .catch to the end of the chain.

Why catch? The code of a promise executor and promise handlers has an “invisible try..catch” around it. If an exception happens, it gets caught and treated as a rejection.

To catch any error not caught by a promise handler, you can catch such errors using the event unhandledrejection:

1
2
3
4
5
6
7
8
9
window.addEventListener("unhandledrejection", function (event) {
  // the event object has two special properties:
  alert(event.promise); // [object Promise] - the promise that generated the error
  alert(event.reason); // Error: Whoops! - the unhandled error object
});

new Promise(function () {
  throw new Error("Whoops!");
}); // no catch to handle the error

Promise API

  • Promise.all(iterable) : wait all promises in iterable are resolved to use the next then.

    • If any of the promises is rejected, the promise returned by Promise.all immediately rejects with that error. Therefore, other promises are ignored.

    • Promise.all(iterable) allows non-promise “regular” values in iterable

      1
      2
      3
      4
      5
      6
      7
      
      Promise.all([
        new Promise((resolve, reject) => {
          setTimeout(() => resolve(1), 1000);
        }),
        2,
        3,
      ]).then(alert); // 1, 2, 3
      
  • Promise.allSettled() : waits for all promises to settle, regardless of individual result.

    • For example, we’d like to fetch the information about multiple users. Even if one request fails, we’re still interested in the others.

    • For each promise, the result looks like this:

      1
      2
      3
      4
      5
      
      [
        {status: 'fulfilled', value: ...response...},
        {status: 'fulfilled', value: ...response...},
        {status: 'rejected', reason: ...error object...}
      ]
      
  • Promise.race: similar to Promise.all but waits only for the first settled promise and gets its result (or error).

    1
    2
    3
    4
    5
    6
    7
    
    Promise.race([
      new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
      new Promise((resolve, reject) =>
        setTimeout(() => reject(new Error("Whoops!")), 2000),
      ),
      new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
    ]).then(alert); // 1
    
  • Promise.any(promises) (recently added method) – waits for the first promise to fulfill, and its result becomes the outcome. If all of the given promises are rejected, AggregateError becomes the error of Promise.any.

Promisification

“Promisification” is a long word for the conversion of a function that accepts a callback into a function that returns a promise.

Check out this full example.

Microtasks

Promise handling is always asynchronous, as all promise actions pass through the internal “promise jobs” queue, also called “microtask queue” (V8 term).

So .then/catch/finally handlers are always called after the current code is finished.

If we need to guarantee that a piece of code is executed after .then/catch/finally, we can add it into a chained .then call.

In most JavaScript engines, including browsers and Node.js, the concept of microtasks is closely tied with the “event loop” and “macrotasks”. As these have no direct relation to promises, they’re covered in another part of the tutorial, in the article Event loop: microtasks and macrotasks.

Async/await

The word “async” before a function means one simple thing: a function always returns a promise IMPLICITLY but we could also return a promise explicitly as well.

The keyword await makes JavaScript wait until that promise settles and returns its result. It works only inside async functions.

So the following doesn’t work:

1
2
3
4
function f() {
  let promise = Promise.resolve(1);
  let result = await promise; // Syntax error
}

In modern browsers, await on top level works just fine, when we’re inside a module.

If we’re not using modules, or older browsers must be supported, there’s a universal recipe: wrapping into an anonymous async function or IIFE.

When we use async/await, we rarely need .then, because await handles the waiting for us. And we can use a regular try..catch instead of .catch. That’s usually (but not always) more convenient.

But at the top level of the code, when we’re outside any async function, we’re syntactically unable to use await, so it’s a normal practice to add .then/catch to handle the final result or falling-through error.

Data Fetching and JSON

One very common asynchronous operation is fetching JSON data from a remote server. The time it takes to receive a response will always be variable and depend on server conditions, network conditions, and more. Learn to use the native fetch function and the JSON object to fetch and parse data.

Fetching Data From the Server

You can use two methods :

  • Fetch API (preferred)
  • XHR API (old)

There is a third way...

In an article, I wrote how you can handle data-fetching in an interesting manner.

Using Fetch

Example:

1
2
3
4
5
6
7
8
9
let response = await fetch(url);

if (response.ok) {
  // if HTTP-status is 200-299
  // get the response body (the method explained below)
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

Or using pure promises syntax:

1
2
3
4
5
6
7
8
fetch(url)
  .then((response) => {
    if (!response.ok) {
      alert("HTTP-Error: " + response.status);
    }
    return response.json();
  })
  .then((json) => console.log(json));

⚠️ We can choose only one body-reading method: response.text()or response.json().

The response contains the headers:

1
2
3
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}

On the request, you can set headers. But there’s a list of forbidden HTTP headers that we can’t set. These headers ensure proper and safe HTTP, so they’re controlled exclusively by the browser.

JSON

Working with JSON

The methods available are:

  • JSON.stringify
  • JSON.parse

You can the first method on these data types:

  • Objects { ... }
  • Arrays [ ... ]
  • Primitives:
    • strings,
    • numbers,
    • boolean values true/false,
    • null.

JSON is data-only language-independent specification, so some JavaScript-specific object properties are skipped by JSON.stringify.

Namely:

  • Function properties (methods).
  • Symbolic keys and values.
  • Properties that store undefined.

The important limitation

No circular references can exist!

Here is the full specs of JSON.stringify:

1
JSON.stringify(value[, replacer, space])

Advanced Array and Object Methods

Advanced Array Methods

Arrays have many powerful methods built in making it easier to accomplish almost anything with a list of items.

Check out MDN > Array.

Advanced Object Methods

Objects have considerably less useful methods available and yet you’ll find yourself reach for a handful of them quite often.

Check out MDN > Objects.

  • Object.entries:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    const object1 = {
      a: "somestring",
      b: 42,
    };
    
    for (const [key, value] of Object.entries(object1)) {
      console.log(`${key}: ${value}`);
    }
    
    // Expected output:
    // "a: somestring"
    // "b: 42"
    
  • Object.assign(target, source1, source2, /* …, */ sourceN) copies all enumerable own properties from one or more source objects to a target object. It returns the modified target object.

  • Object.freeze(obj) prevents object’s extensions and makes existing properties non-writable and non-configurable. The Object.freeze() static method freezes an object. In other words, new properties can’t be added, existing properties can’t be removed, their enumerability, configurability, writability, or value can’t be changed, and the object’s prototype can’t be reassigned. freeze() returns the same object that was passed in.

  • Object.prototype.hasOwnProperty(obj, prop) (or recently renamed Object.prototype.hasOwn(obj, prop)) returns a boolean indicating whether this object has the specified property as its own property (as opposed to inheriting it).

    • .hasOwn() is recommended over .hasOwnProperty(), in browsers where it’s supported.

ES6+ Features

Miscellaneous ES6+ Improvements

JavaScript ES6 brought a wealth of new and useful features over 10 years ago that are still huge timesavers to this day. You should familiarize yourself with the following syntax.

Spread and Rest

⚠️ The rest parameters must be at the end.

1
2
3
function f(arg1, ...rest, arg2) { // arg2 after ...rest ?!
  // error
}

arguments is both array-like and iterable, it’s not an array and so doesn’t support array methods.

Destructuring Arrays

“Destructuring” doesn’t mean “destructive”.

You can ignore elements using commas:

1
2
3
4
5
6
7
8
9
// second element is not needed
let [firstName, , title] = [
  "Julius",
  "Caesar",
  "Consul",
  "of the Roman Republic",
];

alert(title); // Consul

It works with any iterable on the right side, like looping with .entries():

1
2
3
for (let [key, value] of Object.entries(user)) {
  alert(`${key}:${value}`); // name:John, then age:30
}

And you need to know the “Swap variables trick”:

1
2
3
4
5
6
7
let guest = "Jane";
let admin = "Pete";

// Let's swap the values: make guest=Pete, admin=Jane
[guest, admin] = [admin, guest];

alert(`${guest} ${admin}`); // Pete Jane (successfully swapped!)

If the array is shorter than the list of variables on the left, there will be no errors. Absent values are considered undefined. So if you need something else than undefined, use the default value syntax:

1
2
3
4
5
// default values
let [name = "Guest", surname = "Anonymous"] = ["Julius"];

alert(name); // Julius (from array)
alert(surname); // Anonymous (default used)

Destructuring Objects

If we have a complex object with many properties, we can extract only what we need.

If we want to assign a property to a variable with another name, for instance, make options.width go into the variable named w, then we can set the variable name using a colon.

For potentially missing properties we can set default values using "=", similar to arrays.

The ...rest works as well on Objects and must go at the end.

Gotcha if there’s no let : if you declare the object properties before the destructuring and destructure some props of the object using those variables, it’ll fail because { var1, var2, var3 } = myObject; is considered as a variable redeclaration, which isn’t possible:

1
2
3
4
let title, width, height;

// error in this line
{title, width, height} = {title: "Menu", width: 200, height: 100};

So do this instead (kinda of a IIFE):

1
2
3
4
let title, width, height;

// okay now
({ title, width, height } = { title: "Menu", width: 200, height: 100 });

And if you need complex destructuring:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let options = {
  size: {
    width: 100,
    height: 200,
  },
  items: ["Cake", "Donut"],
  extra: true,
};

// destructuring assignment split in multiple lines for clarity
let {
  size: {
    // put size here
    width,
    height,
  },
  items: [item1, item2], // assign items here
  title = "Menu", // not present in the object (default value is used)
} = options;

alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut

Finally, you can use destructuring in the method’s parameters if the argument passed is an object.

JavaScript Modules

JavaScript modules allow us to split up our scripts into various files for the sake of organization and encapsulation. Read up on how they work and how to use them to prepare for the exam.

Checkout JavaScript.info > Modules to learn more.

Here are my summary:

  • Modules always work in strict mode, e.g., assigning to an undeclared variable will throw an error.

  • Each module has its own top-level scope. In other words, top-level variables and functions from a module aren’t seen in other scripts. Unless a given module imports another explicitly. But that other script can only import the first exports.

  • A module is executed only once, on the first import. Exports are generated, and then they’re shared between importers, so if something changes the imported object, other importers will see that.

  • The object import.meta contains the information about the current module.

  • In a module, “this” is undefined.

  • Module scripts are always deferred, in other words:

    • downloading external module scripts <script type="module" src="..."> doesn’t block HTML processing, they load in parallel with other resources.
    • module scripts wait until the HTML document is fully ready (even if they’re tiny and load faster than HTML), and then run.
    • relative order of scripts is maintained: scripts that go first in the document, execute first.

As a side effect, module scripts always “see” the fully loaded HTML page, including HTML elements below them (remember above, a <script> before the </body> can’t see HTML elements between it and the </body>).

Async scripts, external or inline, run immediately when ready, independently of other scripts or the HTML document. But use it on functionality that doesn’t depend on anything.

No “bare” modules are allowed in HTML, though we can find them in NodeJS applications.

What is tree-shaking?

It’s the removal of unused exports, making the JavaScript code deployed lighter.

So on build time, transformations and optimizations may be applied:

  • Unreachable code removed.
  • Unused exports removed (“tree-shaking”).
  • Development-specific statements like console and debugger removed.
  • Modern, bleeding-edge JavaScript syntax may be transformed to an older one with similar functionality using Babel.
  • The resulting file is minified (spaces removed, variables replaced with shorter names, etc).

Export and Import

No semicolons after export class/function, like on Function or Class declaration.

For export [const | class | function] something , the import is import { something } from './my-module.js .

Same in this syntax:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 📁 say.js
function sayHi(user) {
  alert(`Hello, ${user}!`);
}

function sayBye(user) {
  alert(`Bye, ${user}!`);
}

export { sayHi, sayBye }; // a list of exported variables

But if there’s a lot to import, we can import everything as an object using import * as <obj>, for instance.

We can also use as to import under different names.

For instance, let’s import sayHi into the local variable hi for brevity, and import sayBye as bye.

What about export default?

  1. Modules that contain a library, pack of functions are imported as import { constant1, func1, etc } from '...'.

  2. Modules that declare a single entity, e.g. a module user.js exports only class User.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    // user.js
    export default class User {
      // just add "default"
      constructor(name) {
        this.name = name;
      }
    }
    // ...
    // main.js
    import User from "./user.js";
    

⚠️ There may be only one export default per file.

Named exports are explicit. They exactly name what they import, so we have that information from them; that’s a good thing.

But default export can be named to anything… And team members may use different names to import the same thing, and that’s not good.

Usually, to avoid that and keep the code consistent, there’s a rule that imported variables should correspond to file names.

Dynamic Imports

Although import() looks like a function call, it’s a special syntax that just happens to use parentheses (similar to super() in a class constructor).

It can be achieved with the import() expression that returns a Promise.

So we can’t copy import to a variable or use call/apply with it. It’s not a function.

If you need to load a module with a dynamic name, it looks like that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// path/to/module/myFunction.js
export const myFunction = {
  prop1: "Value of prop1",
  prop2: 1,
};

// ...
// in main.js
export async function doSomething(variable) {
  const module = await import(`./path/to/module/${variable}Function.js`);
  return module[`${variable}Function`];
}

// "my" is the value of the parameter "variable"
doSomething("my").then((myFunction) => {
  if (myFunction) {
    console.log(`${myFunction.prop1} / ${myFunction.prop2}`);
  }
});

Error Handling

Handling Errors with Try/Catch

No matter how great we’re at programming, sometimes our scripts have errors. You need to know how to use try/catch to handle them.

For try...catch to work, the code must be runnable. In other words, it should be valid JavaScript.

try...catch works synchronously, so make sure to put try...catch inside the asynchronous code, like setTimeout, otherwise, the error will be hard to catch.

For all built-in errors, the error object has two main properties: nameand message. Sometimes, you have stack properties as well, though it isn’t standard.

We can omit (error) in catch(error) { ... } in modern browser , if we don’t need the error details.

Catch should only process errors that it knows and “rethrow” all others.

To finish, like C#, you can “clean up” after the catch with finally { ... }.

Also, remember that variables are local inside try...catch...finally.

And you can use try...finally without a catch. But then any error fall through!

If you need to catch all errors, then use onerror event:

1
2
3
4
5
6
7
8
9
/**
 * message: Error message.
 * url: URL of the script where error happened.
 * line, col: Line and column numbers where error happened.
 * error: Error object.
 */
window.onerror = function (message, url, line, col, error) {
  // ...
};

Custom Errors

Our unique applications will certainly throw and catch common errors from various APIs and libraries but you’ll also need to create your own Error types to correctly describe issues in your app. Make to know how to extend the Error class.

Extending Error built-in class involves using the extends keyword:

1
2
3
4
5
6
class ValidationError extends Error {
  constructor(message) {
    super(message); // (1)
    this.name = "ValidationError"; // (2)
  }
}

To get instance’s type of the error, use instanceof rather than the err.name == 'MyCustomError'. It’s cleaner and less prone to typing errors.

What is “wrapping exceptions”?

Review this example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class ReadError extends Error {
  constructor(message, cause) {
    super(message);
    this.cause = cause;
    this.name = "ReadError";
  }
}

class ValidationError extends Error {
  /*...*/
}
class PropertyRequiredError extends ValidationError {
  /* ... */
}

function validateUser(user) {
  if (!user.age) {
    throw new PropertyRequiredError("age");
  }

  if (!user.name) {
    throw new PropertyRequiredError("name");
  }
}

function readUser(json) {
  let user;

  try {
    user = JSON.parse(json);
  } catch (err) {
    if (err instanceof SyntaxError) {
      throw new ReadError("Syntax Error", err);
    } else {
      throw err;
    }
  }

  try {
    validateUser(user);
  } catch (err) {
    if (err instanceof ValidationError) {
      throw new ReadError("Validation Error", err);
    } else {
      throw err;
    }
  }
}

try {
  readUser("{bad json}");
} catch (e) {
  if (e instanceof ReadError) {
    alert(e);
    // Original error: SyntaxError: Unexpected token b in JSON at position 1
    alert("Original error: " + e.cause);
  } else {
    throw e;
  }
}

Conclusion

Understanding JavaScript should be an entry gate to the JavaScript world, even before training and working JavaScript framework like Vue, Angular or React.

I did train with Wes Bos and his ES6 and JavaScript 30 courses. So when I started with Vue, I was already familiar with many concepts.

Still, going through the Mid-level JavaScript certification, I learned that I didn’t know everything and that I needed to deepen some concepts.

I recommend this Certificates.dev program (no affiliation, I just like what they do and I have found it very helpful).

Follow me

Thanks for reading this article. Make sure to follow me on X, subscribe to my Substack publication and bookmark my blog to read more in the future.

Credits :

License GPLv3 | Terms
Built with Hugo
Theme Stack designed by Jimmy