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.
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.
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.
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.
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']
|
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.
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.
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.
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.
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:
"Mississippi"
contains the substring ss
.
Mrs
in some text with Ms
.
St
?
art
?
-
).
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.
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
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.
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.
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:
SyntaxError
usually has nothing to do with the values of any of your variables. You can almost always spot the error visually.
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.
SyntaxError
s 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
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.
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.
What does this code log to the console? Why?
let array1 = [1, 2, 3];
let array2 = array1;
array1[1] = 4;
console.log(array2);
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
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)
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
Write some code to output the square root of 37.
> console.log(Math.sqrt(37))
6.082762530298219
= undefined
Video Walkthrough
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 |
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
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.
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
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']
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
What is exception handling and what problem does it solve?
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
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.
function isNotANumber(value) {
return value !== value;
}
This works since NaN
is the only JS value that is not ===
to itself.
Video Walkthrough
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
.
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
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?
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