More Stuff

Thus far, we've discussed JavaScript's most basic features. Let's now turn our attention to several useful topics that don't fit elsewhere. Most of these topics aren't critical to learning JavaScript, but the first -- Variables as Pointers -- most certainly is. With the remaining topics, be aware that you'll encounter them when you read JavaScript code, so, be ready!

Each of these topics has enough depth for a separate book, but we'll stick to the basics and give you a quick introduction to each.

Variables as Pointers

This section will examine the concepts behind variables and pointers. Specifically, we'll see how some variables act as pointers to a place (or address space) in memory while others contain values. Beware: new programmers often have trouble mastering these concepts, but the material is crucial. We'll cover the basics and explore them in greater detail in the Core Curriculum.

Developers sometimes talk about references instead of pointers. At Launch School, we use both terms interchangeably. You can say that a variable points to or references an object in memory, and you can also say that the pointers stored in variables are references. Some languages make a distinction between references and pointers, but JavaScript does not; feel free to use either term.

As we've learned, JavaScript values fall into two broad categories: primitive values and objects. Primitive values are easier to understand, so we'll start there.

Working With Primitive Values

Let's take a quick look at how primitive values and the variables assigned to them relate. Consider the following code:

let count = 1;
count = 2;

On line 1, we declare a variable named count and initialize it to a value of 1, which is a primitive value. Line 2 then reassigns count to a new primitive value, 2.

What does that look like in the computer, however? For starters, every time a JavaScript program creates a new variable, JavaScript allocates a spot somewhere in its memory to hold its value. With (most) primitive values, the variable's actual value gets stored in this allocated memory.

Thus, for example, the count variable may end up at address 0x1234 in the computer's memory. The first line sets the memory at that address to 1. The second line then replaces the value at that address with 2. The process looks something like this:

Code variable address value
let count = 1; count 0x12 1
count = 2; count 0x12 2

Let's see what happens when we have two variables, one of which has been set to the value of the other. Try running this code in node:

> let a = 5
> let b = a
> a = 8
> a
= 8

> b
= 5

This code shouldn't be too surprising. The behavior is similar to what you may have seen in an Algebra course. We initialize a to the value 5, then assign b to the value of a: both variables contain 5 after line 2 runs. It's important to realize that the two variables are at different locations in memory, so their values are independent. Here's what we have in terms of memory:

Code address a a address b b
let a = 5 0x14 5
let b = a 0x14 5 0x76 5

Note that the variables are stored in separate memory addresses, but both have the same value (5).

Next, we reassign variable a to a value of 8 on line 3, and on lines 4 and 5, we see that a does indeed now have the value 8. On lines 7 and 8, we see that b's value did not change: it is still 5. Here's the complete code snippet in tabular form:

Code address a a address b b
let a = 5 0x14 5
let b = a 0x14 5 0x76 5
a = 8 0x14 8 0x76 5

As you can see, each variable has the same value after the second let statement. However, reassigning the a variable to a new value does not affect the b variable. b still has the same value it held initially. The a and b variables are independent: changing one doesn't affect the other.

What's crucial to understand is that variables with primitive values are stored at the memory location associated with the variable. In our example, a and b point to different memory locations. When we assign a value to either variable, the value gets stored in the appropriate memory location. Suppose you later change one of those memory locations. In that case, it does not affect the other memory location, even if they started with the same value. Therefore, the variables are independent when they contain primitive values.

Note that string values are primitive values. For reasons we won't go into here, strings aren't stored directly in variables in the same way as most primitive values. However, they behave as though they are. Don't worry about how they are stored. Just remember that variables with string values behave the same way as those with other primitive values.

Working with Objects and Non-Mutating Operations

Now that we know how variables and primitive values relate let's see how variables and objects relate. Consider the following code:

let obj = { a: 1 };
obj = { b: 2 };

In this example, we declare a variable named obj on line 1 and initialize it to { a: 1 }, which is an object value. Line 2 reassigns obj to a new object, { b: 2 }.

What does that look like in the computer? As we learned earlier, creating new variables causes JavaScript to allocate a spot in its memory for the value. However, with objects, JavaScript doesn't store the object's value in the same place. Instead, it allocates additional memory for the object and places a pointer to the object in the variable. Thus, we need to follow two pointers to get the value of our object from its variable name. The process looks like this:

Code variable address value referenced object
let obj = { a: 1 }; obj 0x1234 0x1120 { a: 1 }
obj = { b: 2 }; obj 0x1234 0x2212 { b: 2 }

In this example, the variable obj is always at address 0x1234. The value at that address is a pointer to the actual object. While the pointer to the object can change -- we can see it change when { b: 2 } is reassigned to obj -- obj itself always has the same address. The value stored at address 0x1234 is a pointer to an object somewhere else in memory. Initially, the pointer references the object { a: 1 }. However, after we reassign obj, the new pointer value references the object { b: 2 }.

Let's look at another example. This time, we'll use arrays. Remember that arrays in JavaScript are objects, and almost everything we say about arrays holds for objects.

> let c = [1, 2]
> let d = c
> c = [3, 4]
> c
= [ 3, 4 ]

> d
= [ 1, 2 ]

For the moment, let's ignore what happens on line 2. We can assume that variables c and d end up with the same value after line 2 runs. Reassigning c on line 3 creates a new array, but the code doesn't affect the value of d. The two variables reference different arrays.

This code works as stated since reassignment changes the pointer value of c to reference the new [3, 4] object. Though d originally had the same pointer value as c, it was stored in a different memory location (the location of d). Thus, when we reassign c, we're not changing d -- it still points to the original array.

As with primitive values, this is straightforward: each variable has a value, and reassigning values does not affect any other variables with the same value. Thus, c and d are independent variables.

Code addr c -> pointer -> object addr d -> pointer -> object
let c = [1, 2] 0x28 -> 0x34 -> [1, 2]
let d = c 0x28 -> 0x34 -> [1, 2] 0x68 -> 0x34 -> [1, 2]
c = [3, 4] 0x28 -> 0x24 -> [3, 4] 0x68 -> 0x34 -> [1, 2]

Let's see what happens with a mutating operation like the push method:

> let e = [1, 2]
> let f = e
> e.push(3, 4)
> e
= [ 1, 2, 3, 4 ]

> f
= [ 1, 2, 3, 4 ]

Now, that's interesting and puzzling. We mutated the array referenced by e, but it also changed the array referenced by f! How can that happen? Therein lies the source of a lot of confusion for new programmers.

As we've seen, objects (and arrays) aren't stored in the memory location used by the variable. Instead, that memory location points to yet another memory location. That's where the object is ultimately stored.

Pointers have a curious effect when you assign a variable that references an object to another variable. Instead of copying the object, JavaScript only copies the pointer. Thus, when we initialize f with e, we're making both e and f point to the same array: [1, 2]. It's not just the same value but the same array in the same memory location. The two variables are independent, but since they point to the same array, that array is dependent on what you do to both e and f.

With e and f pointing to the same array, line 3 uses the pointer in the e variable to access and mutate the array by appending 3 and 4 to its original value. Since f also points to that same array, both e and f reflect the updated contents of the array. Some developers call this aliasing: e and f are aliases for the same value.

Code addr e -> pointer -> object addr f -> pointer -> object
let e = [1, 2] 0x16 -> 0xA4 -> [1, 2]
let f = e 0x16 -> 0xA4 -> [1, 2] 0x80 -> 0xA4 -> [1, 2]
e.push(3, 4) 0x16 -> 0xA4 -> [1, 2, 3, 4] 0x80 -> 0xA4 -> [1, 2, 3, 4]

Okay, that's good. What happens if we mutate a primitive value? Oops! You can't do that: all primitive values are immutable. Two variables can have the same primitive value. However, since primitive values are stored in the memory address allocated for the variable, they can never be aliases for each other. If you give one variable a new primitive value, it doesn't affect the other.

Gotcha

If you've followed along this far, you may think that reassignment never mutates anything. As the following code demonstrates, however, that isn't always true:

> let g = ['a', 'b', 'c']
> let h = g
> g[1] = 'x'
> g
= [ 'a', 'x', 'c' ]

> h
= [ 'a', 'x', 'c' ]

Don't let this confuse you. The critical thing to observe here is that we're reassigning a specific element in the array, not the array itself. This code doesn't mutate the element, but it does mutate the array. Reassignment applies to the item you're replacing, not the object or array that contains that item.

Code addr g -> pointer -> object addr h -> pointer -> object
let g = ['a', 'b', 'c'] 0xB0 -> 0x24 -> ['a', 'b', 'c']
let h = g 0xB0 -> 0x24 -> ['a', 'b', 'c'] 0x44 -> 0x24 -> ['a', 'b', 'c']
g[1] = 'x' 0xB0 -> 0x24 -> ['a', 'x', 'c'] 0x44 -> 0x24 -> ['a', 'x', 'c']

Takeaway

The takeaway of this section is that JavaScript stores primitive values in variables. Still, it uses pointers for non-primitive values like arrays and other objects. Pointers can lead to surprising and unexpected behavior when two or more variables reference the same object in the heap. Primitive values don't have this problem.

When using pointers, it's also essential to remember that some operations mutate objects while others don't. For instance, push mutates an array, but map does not. In particular, you must understand how something like x = [1, 2, 3] and x[2] = 4 differ: both are reassignments, but the second mutates x while the first does not.

That's all you need to know for now. We can guarantee that you'll run into bugs related to this topic. Don't try to memorize the rules right now, though; we'll return to them in the Core Curriculum. For now, learn the basic concepts, then use them and your development tools to reason about how your program works.

The idea that JavaScript stores primitive values directly in variables is an oversimplification of how it works in the real world. However, it mirrors reality well enough to serve as a mental model for almost all situations. Don't sweat the details right now.

for/in and for/of

Two useful variants for the for loop are the for/in and for/of loops. These loops use a variant syntax to loop easily over object properties.

The for/in statement iterates over all enumerable properties of an object including any properties inherited from another object. For now, you don't need to know anything about inheritance or enumerable properties -- for/in will usually do what you want.

For instance:

let obj = { foo: 1, bar: 2, qux: 'c' };
for (let key in obj) {
  console.log(key);
}
// Output:  foo
//          bar
//          qux

As we learned earlier, arrays are also objects, so we can use for/in to iterate over arrays. However, the results may not be exactly what you expect:

let arr = [ 10, 20, 30 ]
for (let value in arr) {
  console.log(value);
}
// Output:  0
//          1
//          2

As you can see, it iterates over the index values -- those are the keys from the array (as strings!). Unfortunately, using the index values can lead to trouble - the index values here are strings!

let arr = [ 10, 20, 30 ]
for (let index in arr) {
  console.log(index + 5);
}
// Output:  05
//          15
//          25

For this reason (and others), you should never use for/in to iterate over an array.

A more direct way to iterate over the values in an array is to use for/of:

let arr = [ 10, 20, 30 ]
for (let value of arr) {
  console.log(value);
}
// Output:  10
//          20
//          30

for/of is similar to for/in, but it iterates over the values of any "iterable" collection. For our purposes, the only iterable collections are arrays and strings. Let's see what happens when we pass a string to for/of:

let str = "abc";
for (let char of str) {
  console.log(char);
}
// Output: a
//         b
//         c

The for/in statement has been in JavaScript since its earliest days, so is available in all but a handful of ancient JS implementations. The for/of statement was added in ES6, so is only available in relatively modern implementations.

Method Chaining

From time to time, you may see code that looks like this:

let str = 'Pete Hanson';
let names = str.toUpperCase().split(' ').reverse().join(', ');
console.log(names); // => HANSON, PETE

On line 2, we have a long chain of method calls. First, we call toUpperCase() on the string str, which returns 'PETE HANSON'. Then we call split(' ') on the returned string, which in turn returns the array ['PETE', 'HANSON']. We then use the array to invoke reverse(), which returns a reference to the mutated array ['HANSON', 'PETE']. In the last step, we join the elements of the array together with a comma and space between elements, which returns the string 'HANSON, PETE'.

You'll see this style of coding often as you learn more about JavaScript: it's called method chaining. For now, you don't need to know how to write code like that, but you should be able to read and understand it. The main takeaway is that you can call a method on the return value of another method. It's not much different from function composition, but it uses a simpler syntax.

You'll also see several syntactic variations on this code:

let str = 'Pete Hanson';
let names = str.toUpperCase()
               .split(' ')
               .reverse()
               .join(', ');
console.log(names);
let str = 'Pete Hanson';
let names = str.toUpperCase()
  .split(' ')
  .reverse()
  .join(', ');
console.log(names);
let str = 'Pete Hanson';
let names = str.toUpperCase().
                split(' ').
                reverse().
                join(', ');
console.log(names);

All of these (and more) are acceptable. The main advantage to these alternatives is improved readability.

Optional Chaining

A variation on chaining is optional chaining. In optional chaining, the chaining continues from left to right, just as in ordinary chaining. However, if any of the intermediate values is nullish (null or undefined), the chain short-circuits and returns undefined.

Optional chaining is most often used early in a chain when the first part of the expression may return a nullish value. For instance, consider the following code:

function reverse_words(sentence) {
  return sentence.split(' ')
                 .reverse()
                 .join(' ');
}

This function works well if you pass it any kind of string. For instance:

console.log(reverse_words("Four score and seven"))
// seven and score Four

However, suppose there's a chance that sentence might be nullish? What happens then?

console.log(reverse_words(null))
// Uncaught TypeError: Cannot read properties of null (reading 'split')

One way around that is to use a guard clause:

function reverse_words(sentence) {
  if (sentence === null || sentence === undefined) {
    return undefined;
  }

  return sentence.split(' ')
                 .reverse()
                 .join(' ');
}

This works with both actual strings and nullish values, but guard clauses can quickly become tedious to write. That's where optional chaining comes in:

function reverse_words(sentence) {
  return sentence?.split(' ')
                  .reverse()
                  .join(' ');
}

In this code, the ?. operator performs optional chaining. If sentence is nullish, the entire chain evaluates as undefined. Otherwise, the expression is evaluated as expected (assuming sentence is a string):

console.log(reverse_words("Four score and seven"));
// seven and score Four

console.log(reverse_words(null));          // undefined
console.log(reverse_words(undefined));     // undefined

It's worth noting that you should only use optional chaining to achieve a specific result. In many cases, an error is better than an undefined return value. There's a tendency for undefined return values to go unchecked by the calling code, which may lead to subtle errors much later in the code.

Regex

A regular expression -- a regex -- is a sequence of characters that you can use to test whether a string matches a given pattern. They have a multitude of uses:

  • Check whether the string "Mississippi" contains the substring ss.
  • Print the 3rd word of each sentence from a list of sentences.
  • Replace all instances of Mrs in some text with Ms.
  • Does a string begin with the substring St?
  • Does a string end with the substring art?
  • Does a string contain any non-alphanumeric characters?
  • Does a string contain any whitespace characters?
  • Replace all non-alphanumeric characters in a string with a hyphen (-).

That's a tiny sample of the kinds of operations you can perform with regex.

Since regexes is a bit of a mouthful, Launch School uses the term regex as both a plural and singular noun.

This book doesn't try to teach you how to read and write regex; instead, it focuses on the most common use case: determining whether a string matches a given pattern.

A regex looks like a string written between a pair of forward-slash characters instead of quotes, e.g., /bob/. You can place any string you want to match between the slashes, but certain characters have special meanings. We won't discuss those special meanings, but we'll see some simple examples.

JavaScript uses RegExp objects to store regex: note the spelling and case. Like other objects, RegExp objects can invoke methods. The method test, for instance, returns a boolean value based on whether a string argument matches the regex. Here's how we can use test to determine whether the string "bobcat" contains the letter o or l:

> /o/.test('bobcat')
= true

> /l/.test('bobcat')
= false

As you might expect, the first test returned true since o is present in the string, but the second returns false since "bobcat" doesn't contain the letter l. You can use these boolean values to perform some operation depending on whether a match occurs:

if (/b/.test('bobcat')) {
  // this branch executes since 'b' is in 'bobcat'
  console.log("Yes, it contains the letter 'b'");
} else {
  // this branch does not execute since 'bobcat' contains 'b'
  console.log("No, it doesn't contain the letter 'b'");
}

Boolean values sometimes don't provide enough information about a match. That's when the match method for strings comes in handy. This method takes a regex as the argument and returns an array that describes the match.

> "bobcat".match(/x/)         // No match
= null

> "bobcat".match(/[bct]/g)    // Global match
= [ 'b', 'b', 'c', 't' ]

> "bobcat".match(/b((o)b)/)   // Singular match with groups
= [ 'bob', 'ob', 'o', index: 0, input: 'bobcat', groups: undefined ]

Don't worry if you don't understand those last two regex. Focus on the meaning of the return values, not the regex.

If no match occurs, match returns the value null, which conveniently lets us use match in conditionals in the same way as test. We'll see that in action a little further down.

When a match occurs with a regex that contains the /g flag -- a global match -- the match method returns an array that contains each matching substring. The /g example above returns an array consisting of the matched b (twice, since it appears twice in the string), c, and t letters.

When /g isn't present, the return value for a successful match is also an array, but it includes some additional properties:

  • index: the index within the string where the match begins
  • input: a copy of the original string
  • groups: used for "named groups" (we don't discuss named groups in this book)

The array elements are also a bit different when /g isn't present. In particular, the first element (bob in the above example) represents the entire matched part of the string. Additional elements (ob and o in the example) represent capture group matches. Parentheses inside a regex define capture groups.

We discuss /g and capture groups in our core curriculum. You don't have to understand them right now, but it's important to remember how they influence the return value of match.

As mentioned above, match returns null when a match doesn't occur. You can harness this in conditionals:

function has_a_or_e(string) {
  let results = string.match(/[ae]/g);
  if (results) {
    // a non-null return value from match is truthy
    console.log(`We have a match! ${results}`);
  } else {
    // a null return value from match is falsy
    console.log('No match here.');
  }
}

has_a_or_e("basketball"); // => We have a match! a,e,a
has_a_or_e("football");   // => We have a match! a
has_a_or_e("hockey");     // => We have a match! e
has_a_or_e("golf");       // => No match here.

We've used a snake_case name (has_a_or_e) instead of a camelCase name for clarity; it's hard to write a camelCased function name that describes what this function does. Case variations like this aren't common, and you should continue to use camelCase in almost all of your code.

Since match must generate information above and beyond a simple boolean value, it can have performance and memory costs. test is more efficient, so try to use it when you don't need to know anything other than whether the regex matched the string.

Using /g in conjunction with test can have confusing results. Consider the following code:

let regex = /b/g;
let str = "ababa";

console.log(regex.test(str)); // => true
console.log(regex.test(str)); // => true
console.log(regex.test(str)); // => false

Many students look at this code and are surprised that it logs true the first 2 times it invokes test, but false the 3rd time. Take a moment to think about this. Why do you think that happens? Don't worry if you don't get it right.

The issue here is the /g flag passed to the regex; JavaScript is going to look for every match in the string. However, test only consumes one of the matches at a time. Since there are two occurrences of b in the string, the first two invocations of test return true. The 3rd invocation, however, returns false since there are no more matches after the first two.

Interestingly, the next three invocations of test repeat this cycle:

console.log(regex.test(str)); // => true
console.log(regex.test(str)); // => true
console.log(regex.test(str)); // => false

The moral of this story is that mixing /g and test may lead to surprising results. You may be better off using match instead, or don't use /g in the regex (many students use /g when they don't need to). Keep in mind whether you need all matches or just a single match - if you just need a single match, /g is inappropriate.

As a beginner, you probably won't use regex often. However, keep them in mind: they are the perfect solution to a whole class of problems. A regex can, in a single-line, solve problems that may require dozens of lines using other techniques. If you encounter a string matching problem that needs more than a simple substring search using the indexOf or includes method, remember to look into using regex. However, don't get carried away: don't use regex because you can; use them when they yield simpler and more understandable solutions.

If you want to dive deeper into regex, our Introduction to Regular Expressions book is a great place to start. We ask students to read the book while going through the Core Curriculum, but you're welcome to read it anytime once you know some basics.

The Math Object

Most programs need to perform some arithmetic or mathematical operations. That doesn't mean you need much math as a programmer; most programs need little more than some basic arithmetic like a + 1.

However, sometimes you need a bit more: you may need to calculate the square root of a number, determine the smallest number in a collection, or even do some trigonometric calculations. You can use well-known algorithms to make these computations, but you don't have to. The JavaScript Math object provides a collection of methods and values that you can use without a complete understanding of how they work.

Let's say you want to calculate the square root of a number. It's possible to design and implement an algorithm that calculates the root. Why bother, however? JavaScript's Math object has a method named sqrt that you can use without first designing, writing, and testing some code:

> Math.sqrt(36)
= 6

> Math.sqrt(2)
= 1.4142135623730951

Perhaps you need to use the number π (pi) for something. Again, you can calculate π with one of the available algorithms. However, the Math object's PI property gives you immediate access to a reasonably precise approximation of its value:

> Math.PI
= 3.141592653589793

Dates

Suppose you want to determine the day of the week that December 25 occurred on in 2012. How would you go about that? You may be able to come up with an appropriate algorithm on your own, but working with dates and times is a messy process. It's often much harder than you think.

You don't have to work that hard, however. JavaScript's Date constructor creates objects that represent a time and date. The objects provide methods that let you work with those values. In particular, it's not hard to determine the day of the week that corresponds to a date:

> let date = new Date('December 25, 2012')
> date.getDay()
= 2

getDay returns a number for the day of the week: 0 represents Sunday, 1 represents Monday, and so on. In this case, we see that December 25, 2012, occurred on a Tuesday.

Getting a day name takes a bit more work, but it's not difficult:

function getDayOfWeek(date) {
  let daysOfWeek = [
    'Sunday',
    'Monday',
    'Tuesday',
    'Wednesday',
    'Thursday',
    'Friday',
    'Saturday',
  ];

  return daysOfWeek[date.getDay()];
}

let date = new Date('December 25, 2012');
console.log(getDayOfWeek(date)); // => Tuesday

The getDay method is one of a host of convenient date methods, far more than you'll probably ever use. They can be a bit tricky to use at times, but you'll be happy to learn about them when you need them: working with dates and times is hard enough without compounding the problem by having to write your own code.

After seeing that we had to implement getDayOfWeek(), you might think that JavaScript's developers somehow forgot to include such a useful method. They did, at least in the earliest versions of JavaScript. These days, you can use the toLocaleDateString method of the Date type. It's a bit awkward to use, but it has multi-language support and a host of other features. However, full support may be lacking in some browsers.

Exceptions

Applications that interact with the real world encounter a significant degree of unpredictability. If a user enters incorrect information or a file gets corrupted, your program must know how to respond. If it doesn't, it may crash or, worse yet, produce incorrect results.

JavaScript is a forgiving language. It doesn't issue error messages in scenarios that most other languages do. Instead, it "fails silently" by returning a value like NaN, undefined, null, or even -1.

Silent failures are both useful and dangerous. A programmer can take advantage of silent errors to simplify some code; often, you don't have to deal with the silent error right away, but can postpone handling it or even ignore it entirely. Ultimately, though, you need to deal with errors somehow, even silent errors.

Not all errors in JavaScript are silent. There are some situations where JavaScript is less forgiving; that's where exceptions come into play. In such cases, JavaScript raises an error, or throws an exception, then halts the program if the program does not catch the exception.

Exception handling is a process that deals with errors in a manageable and predictable manner. For now, you should be familiar with how exception handling works and what it looks like in a program. The reserved words try and catch (and sometimes finally) often occur in real-world JavaScript programs, so you should learn enough to understand what they do.

JavaScript's try/catch statement provides the means to handle exceptions. The basic structure looks like this:

try {
  // perform an operation that may produce an error
} catch (exception) {
  // an error occurred. do something about it.
  // for example, log the error
} finally {
  // Optional 'finally' block; not used often
  // Executes regardless of whether an exception occurs.
}

We don't discuss or use the finally clause in this book. You can read about it at MDN.

Let's look at a typical situation. One common JavaScript error occurs when we call a method on the values undefined or null. Look at the following code and test it in Node or your browser's console:

let names = ['bob', 'joe', 'steve', undefined, 'frank'];
names.forEach(name => {
  console.log(`${name}'s name has ${name.length} letters in it.`);
}); // => bob's name has 3 letters in it.
    //    joe's name has 3 letters in it.
    //    steve's name has 5 letters in it.
    //    TypeError: Cannot read property 'length' of undefined
    //        at names.forEach (repl:2:42)
    //        at Array.forEach (<anonymous>)

This program raises an error when it tries to access the length property on the undefined value at names[3]. It then prints a stack trace and halts program execution; it ignores the last entry in the array.

Let's add some exception handling to this program:

let names = ['bob', 'joe', 'steve', undefined, 'frank'];

names.forEach(name => {
  #highlight
  try {
    console.log(`${name}'s name has ${name.length} letters in it.`);
  } catch (exception) {
    console.log('Something went wrong');
  }
  #endhighlight
}); // => bob's name has 3 letters in it.
    //    joe's name has 3 letters in it.
    //    steve's name has 5 letters in it.
    #highlight
    //    Something went wrong
    //    frank's name has 5 letters in it.
    #endhighlight

To handle the possibility of an exception within the callback to forEach, we place the try block inside the callback. We can put any amount of code in the try block, but most often you want to focus on one or two statements.

When we try to use the length property on undefined, JavaScript raises an error like it did before. This time, though, it catches the exception and executes the catch block. When the catch block ends, the program resumes running with the code that follows the entire try/catch statement.

Note that JavaScript runs the catch block when an exception occurs, but not when an exception doesn't occur. Either way, execution ultimately resumes with the code after the try/catch statement.

Don't try to catch every possible exception. If you can't do anything useful with the exception, let it go. Mishandling an exception is usually far more catastrophic than just letting the program fail.

It's also possible to raise your own exceptions. For instance:

function foo(number) {
  if (typeof number !== "number") {
    throw new TypeError("expected a number");
  }

  // we're guaranteed to have a number here
}

The throw keyword raises an exception of the type specified as an argument, which is usually given as new followed by one of the Error types described on this page. In this case, we use a TypeError to indicate that we were expecting a different type for the number argument.

Don't raise exceptions for preventable conditions. Exceptions are for exceptional circumstances: situations that your program can't control very easily, such as not being able to connect to a remote site in a web application. The example shown above that tests the argument type is probably not something that you want to do in a real application. Instead, your code should never call foo with a non-numeric argument, or you should return some sort of error indicator like null or undefined.

If this is confusing, don't worry: exceptions are difficult to understand fully. You can revisit this section later when you've learned more about JavaScript; it should be easier to comprehend when you have some experience. For now, all you need to understand is that you can anticipate and handle errors that may occur in your program; a single unexpected input or other issue doesn't have to crash your entire application or introduce subtle bugs.

SyntaxError

A special kind of exception occurs if the code can't be handled as valid JavaScript. Such errors cause JavaScript to raise a SyntaxError. A SyntaxError is special in that it occurs immediately after loading a JavaScript program, but before it begins to run. Unlike a TypeError exception that is dependent upon runtime conditions, JavaScript detects syntax errors based solely on the text of your program. Since they are detected before execution begins, you can't use a try/catch statement to catch one.

Here's some code that will cause a syntax error:

console.log("hello");

function foobar()
  // some code here
}

foobar();
}
^

SyntaxError: Unexpected token '}'

Since the SyntaxError gets raised before the program starts running, the console.log on line 1 never gets executed. In addition, the foobar function never gets invoked. As soon as JavaScript spots the error, it raises the SyntaxError exception.

There are three major takeaways from the above example:

  1. A SyntaxError usually has nothing to do with the values of any of your variables. You can almost always spot the error visually.
  2. A SyntaxError can occur long after the point where the error was. In the above example, the error is on line 3 (a missing {), but the problem is reported on line 5. There can be many hundreds of lines between the point where the error is and the point where JavaScript detects it. Unfortunately, that's more common than you might think, so be prepared for it.
  3. The code before and after the error does not run. That's because SyntaxErrors are detected before a program begins running. This also shows that there are at least two phases in the life of a program -- a preliminary phase that checks for syntax errors, and an execution phase.

There are some situations where JavaScript can throw a SyntaxError after a program begins running. For instance, this code raises a SyntaxError at runtime:

JSON.parse('not really JSON');  // SyntaxError: Unexpected token i in JSON at position 0

Stack Traces

In the previous section, we saw that JavaScript exceptions issue error messages that look something like this:

TypeError: Cannot read property 'length' of undefined
    at names.forEach (repl:2:42)
    at Array.forEach (<anonymous>)

This error message is a stack trace: it reports the type of error that occurred, where it occurred, and how it got there. Such error messages rely on JavaScript's call stack, which we discussed in the Functions chapter.

Let's look at a simpler example. Create a file named error.js with the following content:

function foo() {
  console.log(bar);
}

foo();

Now, run it with Node:

$ node error.js
/Users/wolfy/tmp/x.js:2
  console.log(bar);

ReferenceError: bar is not defined
    at foo (error.js:2:15)
    at Object.<anonymous> (error.js:5:1)
    ...

In this example, JavaScript raises a ReferenceError exception since the variable bar doesn't exist when you try to write it to the log. From the stack trace, we can see that JavaScript detected the error at character 15 on line 2 -- that's where we mention the bar variable -- in the foo function. The rest of the trace tells us that we called foo on line 5 from an anonymous function: one with no name. The trace treats code at the global level as belonging to an anonymous function, so don't worry about the fact that your code doesn't actually have an anonymous function.

If your program uses libraries like Handlebars and jQuery, the stack trace may contain hundreds of lines. Even using node to run this simple program adds around 10 additional lines to the trace. In most cases, you can limit your attention to the lines that mention your JavaScript code file(s) by name: error.js in this case. Each filename in the trace includes a location specified as a line number and column number. The file name, line number, and column number together pinpoint the specific location where the failure occurred and how the program reached that point. Take note of the locations that pertain to your code.

We call this type of output a stack trace since the JavaScript (and most other languages) handle the mechanics of calling functions with a data structure known as the call stack. Each time the program calls a function, JavaScript places some information about the current program location on the top of the call stack. When the program finishes running the function, it removes the corresponding item from the top of the stack and uses it to return to the calling location. The stack trace is a readable version of the call stack's content at the point an exception occurred.

We'll return to the call stack shortly. For now, the takeaway is that JavaScript uses it to display the stack trace when an exception occurs. Knowing how to use this information is invaluable when you have to debug a program.

A word of advice: use your stack traces. Make sure you understand what they are saying, and look at the code that it identifies as the failure point. If you don't use the trace, you may introduce more problems in the code, or worse yet, "fix" code that already works. The stack trace lets you focus on the right part of the program.

ES6 and Beyond

Most professionals call the language we've been learning JavaScript, but the official name is ECMAScript. The JavaScript name exists for historical reasons.

The language has seen numerous revisions and experienced a host of changes since its initial version. ECMAScript 6, or ES6 as it's commonly known, is a recent version of the language specification that added a variety of modern features. You may also encounter the name ES2015.

The let and const keywords we've used in this book are part of ES6. Before ES6, JavaScript didn't have block scopes. All JavaScript variables were either locally scoped to a function or globally scoped to the program. These keywords solve an entire class of problems having to do with scope and how JavaScript translates code into something it can run.

Another ES6 feature that we learned about in this book is arrow functions. Among other benefits, they solve a problem called lost execution context, or, more plainly, context loss.

In addition to these features, ES6 introduced a host of other useful features intended to make the language more expressive, secure, and easier to use. The language doesn't stop at ES6, however. ECMAScript is an evolving language. The committee that oversees the evolution of the language accepts proposals from everywhere and adds the features that they think are useful.

The continuous evolution of JavaScript means that some JavaScript environments may not be up-to-date, and may lack some recent features. This situation has lead to the development of tools that let you write code using the latest language features and then run it -- after a suitable translation step -- in a less current environment. Babel is one such tool. You can try an online version that lets you write and convert programs online.

We introduce more ES6 features in our Core Curriculum at Launch School.

Exercises

  1. What does this code log to the console? Why?

    let array1 = [1, 2, 3];
    let array2 = array1;
    array1[1] = 4;
    console.log(array2);
    

    Solution

    The code outputs:

    [ 1, 4, 3]
    

    This result demonstrates that array1 and array2 reference the same array: if we change an element using array1, it also changes that element in array2. The opposite is also true: if we change an element in array2, that also changes the element in array1.

    This code also demonstrates that assignment of an array to another array doesn't create a new array, but instead copies a reference from the original array (array1 above) into the target array (array2).

    > array1[1] = 4
    = 4
    
    > array1
    = [ 1, 4, 3 ]
    
    > array2
    = [ 1, 4, 3 ]
    

    Video Walkthrough

    Please register to play this video

  2. What do the following error message and stack trace tell you?

    $ node exercise2.js
    /Users/wolfy/tmp/exercise2.js:4
      console.log(greeting);
                  ^
    
    ReferenceError: greeting is not defined
        at hello (/Users/wolfy/tmp/exercise2.js:4:15)
        at Object.<anonymous> (/Users/wolfy/tmp/exercise2.js:13:1)
        at Module._compile (internal/modules/cjs/loader.js:721:30)
        at Object.Module._extensions..js (internal/modules/cjs/loader.js:732:10)
        at Module.load (internal/modules/cjs/loader.js:620:32)
        at tryModuleLoad (internal/modules/cjs/loader.js:560:12)
        at Function.Module._load (internal/modules/cjs/loader.js:552:3)
        at Function.Module.runMain (internal/modules/cjs/loader.js:774:12)
        at executeUserCode (internal/bootstrap/node.js:342:17)
        at startExecution (internal/bootstrap/node.js:276:5)
    

    Solution

    An error occurred in the exercise2.js program on line 4 of the program; a ^ points to where JavaScript thinks the error is in the code: it's pointing to the argument list for console.log.

    More specifically, line 6 in the output tells you that a ReferenceError exception occurred and that the name greeting isn't defined. Line 7 repeats some earlier information: JavaScript detected the error at column 15 of line 4 of the program, but it also tells you that the code is in the hello function. Line 8 tells you that hello was called from line 13 of the program in an anonymous function, namely the global-level of the program.

    The rest of the output comes from running the code in node and probably isn't useful to you as an application programmer.

    Video Walkthrough

    Please register to play this video

  3. Write some code to output the square root of 37.

    Solution

    > console.log(Math.sqrt(37))
    6.082762530298219
    = undefined
    

    Video Walkthrough

    Please register to play this video

  4. Given a list of numbers, write some code to find and display the largest numeric value in the list.

    List Max
    1, 6, 3, 2 6
    -1, -6, -3, -2 -1
    2, 2 2

    Solution

    console.log(Math.max(1, 6, 3, 2));      // => 6
    console.log(Math.max(-1, -6, -3, -2));  // => -1
    console.log(Math.max(2, 2));            // => 2
    

    Video Walkthrough

    Please register to play this video

  5. What does the following function do?

    function doSomething(string) {
      return string.split(' ').reverse().map((value) => value.length);
    }
    

    Don't hesitate to use the MDN Documentation.

    Solution

    This code converts a string into an array of words, reverses that array, and then returns a new array that contains the lengths of the words. It assumes that a single space character delimits the words in the original string.

    Thus:

    console.log(doSomething("Pursuit of happiness")); // => [ 9, 2, 7 ]
    

    Video Walkthrough

    Please register to play this video

  6. Write a function that searches an array of strings for every element that matches the regular expression given by its argument. The function should return all matching elements in an array.

    Example

    let words = [
      'laboratory',
      'experiment',
      'flab',
      'Pans Labyrinth',
      'elaborate',
      'polar bear',
    ];
    
    console.log(allMatches(words, /lab/)); // => ['laboratory', 'flab', 'elaborate']
    

    Solution

    function allMatches(words, pattern) {
      let matches = [];
      for (let index = 0; index < words.length; index += 1) {
        if (pattern.test(words[index])) {
          matches.push(words[index]);
        }
      }
    
      return matches;
    }
    
    function allMatches(words, pattern) {
      return words.filter((word) => pattern.test(word));
    }
    

    Video Walkthrough

    Please register to play this video

  7. What is exception handling and what problem does it solve?

    Solution

    Exception handling is a process that deals with errors in a manageable and predictable manner. It uses the try/catch statement to catch exceptions that the code in the try block raises, and lets the programmer deal with the problem in a way that makes sense and perhaps prevents a catastrophic failure or nasty bug.

    Video Walkthrough

    Please register to play this video

  8. Challenging Exercise This exercise has nothing to do with this chapter. Instead, it uses concepts you learned earlier in the book. If you can't figure out the answer, don't worry: this question can stump developers with more experience than you have.

    Earlier, we learned that Number.isNaN(value) returns true if value is the NaN value, false otherwise. You can also use Object.is(value, NaN) to make the same determination.

    Without using either of those methods, write a function named isNotANumber that returns true if the value passed to it as an argument is NaN, false if it is not.

    Solution

    function isNotANumber(value) {
      return value !== value;
    }
    

    This works since NaN is the only JS value that is not === to itself.

    Video Walkthrough

    Please register to play this video

  9. Challenging Exercise This exercise has nothing to do with this chapter. Instead, it uses concepts you learned earlier in the book. If you can't figure out the answer, don't worry: this question can stump developers with more experience than you have.

    Earlier, we learned that JavaScript has multiple versions of the numeric value zero. In particular, it has 0 and -0. While it's mathematically nonsensical to distinguish between 0 and -0, they are distinct values in JavaScript. We won't get into why JavaScript has a 0 and -0, but it can be useful in some cases.

    There's a problem, however: JavaScript itself doesn't seem to realize that the values are distinct:

    > 0 === -0
    = true
    
    > String(-0)
    = '0'
    

    Fortunately, you can use Object.is to determine whether a value is -0:

    > let value = -0;
    > Object.is(value, 0)
    = false
    
    > Object.is(value, -0)
    = true
    

    There are other ways to detect a -0 value. Without using Object.is, write a function that will return true if the argument is -0, and false if it is 0 or any other number.

    What happens if you divide a non-zero integer by zero? Apply this to the problem of determining whether a value is -0.

    Solution

    function isNegativeZero(value) {
      return 1 / value === -Infinity;
    }
    

    This works since 1 / 0 returns Infinity and 1 / -0 returns -Infinity, thus letting us make the distinction. You can divide any other number except 0 or -0 to achieve the same result.

    You can be a little more explicit with your answer as well:

    function isNegativeZero(value) {
      return (value === 0) && (1 / value === -Infinity);
    }
    

    While this is a little more complex, it clearly shows that we're only interested in numbers that are 0 (or -0), which also helps eliminate unwanted division operations, which may be needed for performance reasons.

    Video Walkthrough

    Please register to play this video

  10. Challenging Exercise This exercise has nothing to do with this chapter. Instead, it uses concepts you learned earlier in the book. If you can't figure out the answer, don't worry: this question can stump developers with more experience than you have.

    Consider this code:

    > let x = "5"
    > x = x + 1
    = "51"
    

    Now, consider this code:

    > let y = "5"
    > y++
    

    What gets returned by y++ in the second snippet, and why?

    Solution

    The return value is the numeric value 5.

    If you apply ++ to a string, JavaScript coerces it into a number. In this case, "5" gets coerced to the number 5. After coercion, it then increments the value to 6. However, the return value is 5 since the post-increment operator (y++) returns the original value of y, not the incremented value.

    This shows that x++ is not the same thing as x = x + 1.

    Video Walkthrough

    Please register to play this video