• Avoiding those dang cannot read property of undefined errors
  • By Adam Giese
  • Translation from: The Gold Project
  • This article is permalink: github.com/xitu/gold-m…
  • Translator: Xcco
  • Proofreader: Hanxiansen, Mirosalva

Uncaught TypeError: Cannot read property ‘foo’ of undefined. Is a terrible mistake we’ve all come across in JavaScript development. Perhaps an API returned an unexpected null value, or something else, but the error is so common and widespread that it’s impossible to tell.

I recently had a problem with an environment variable that for some reason was not loaded, causing all sorts of errors to pop up in front of me. Whatever the reason, leaving this error unaddressed can be catastrophic. So how do we stop this problem from happening at the source?

Let’s find a solution.

Tool library

If you’re already using a library in your project, chances are the library already has functions to prevent this from happening. Get (documentation) in Lodash or R.path(documentation) in Ramda ensure that you can use objects safely.

If you already use the library, this may seem like the easiest way to do it. If you don’t use the tool library, read on!

Use && short circuit

One interesting fact about logical operators in JavaScript is that they don’t always return booleans. According to the instructions, “&& and | | operators return values is not necessarily a Boolean value. But one of the two operations expression.”

As an example of the && operator, if the Boolean value of the first expression is false, the value will be returned. Otherwise, the value of the second expression is used. This means that the expression 0 && 1 returns 0 (a false value), and the expression 2 && 3 returns 3. If multiple ampersand expressions are joined together, they will return the first false plant or the last value. For example, 1 && 2 && 3 && NULL && 4 will return null, and 1 && 2 && 3 will return 3.

So how do you safely get the properties of nested objects? Logical operators in JavaScript are “short-circuited”. In the ampersand case, this means that the expression will stop at the first false value.

const foo = false && destroyAllHumans();
console.log(foo); // falseMankind is safeCopy the code

In this example, destroyAllHumans will not be called because && stops all operations after false

This can be used to safely get the properties of nested objects.

Const meals = {breakfast: null, // I skipped the most important meal of the day! :( lunch: { protein:'Chicken',
    greens: 'Spinach',
  },
  dinner: {
    protein: 'Soy',
    greens: 'Kale',}}; const breakfastProtein = meals.breakfast && meals.breakfast.protein; // null const lunchProtein = meals.lunch && meals.lunch.protein; //'Chicken'
Copy the code

In addition to simplicity, one of the main advantages of this approach is its brevity when dealing with less nesting. However, it can become very verbose when accessing deep objects.

const favorites = {
  video: {
    movies: ['Casablanca'.'Citizen Kane'.'Gone With The Wind'],
    shows: ['The Simpsons'.'Arrested Development'],
    vlogs: null,
  },
  audio: {
    podcasts: ['Shop Talk Show'.'CodePen Radio'], audiobooks: null,}, reading: null, // Just kidding -- I love reading}; const favoriteMovie = favorites.video && favorites.video.movies && favorites.video.movies[0]; // Casablanca const favoriteVlog = favorites.video && favorites.video.vlogs && favorites.video.vlogs[0];
// null
Copy the code

The deeper the object is nested, the bulkier it becomes.

“Or unit”

Oliver Steele proposed this method and explored it in more detail in his blog post, “Unit Chapter 1: Or Unit,” which I’ll try to give a brief explanation of here.

const favoriteBook = ((favorites.reading||{}).books||[])[0]; // undefined
const favoriteAudiobook = ((favorites.audio||{}).audiobooks||[])[0]; // undefined
const favoritePodcast = ((favorites.audio||{}).podcasts||[])[0]; // 'Shop Talk Show'
Copy the code

Similar to the short circuit example above, this method works by checking whether the value is false. If the value is false, it tries to get the properties of the empty object. In the example above, the value of Favorites. Reading is null, so we get the books property from an empty object. This will return an undefined result, so 0 will be used to get the members of the empty array.

The advantage of this approach over the && method is that it avoids the duplication of attribute names. In deeply nested objects, this can be a significant advantage. The main drawback is readability – this is not a normal model, so it may take a little time for the reader to understand how it works.

try/catch

JavaScript in the try… Catch is another safe way to get an attribute.

try {
  console.log(favorites.reading.magazines[0]);
} catch (error) {
  console.log("No magazines have been favorited.");
}
Copy the code

Unfortunately, in JavaScript, try… Catch declarations are not expressions, and they do not evaluate values as they do in some languages. This prevents a simple try declaration from being used as a method to set variables.

One option is to try… Define a let variable before a catch.

letfavoriteMagazine; try { favoriteMagazine = favorites.reading.magazines[0]; } catch (error) { favoriteMagazine = null; /* Any default value can be used */};Copy the code

While this is verbose, it works for setting single variables (that is, if the variables don’t scare you off already). However, writing them all together can cause problems.

let favoriteMagazine, favoriteMovie, favoriteShow;
try {
  favoriteMovie = favorites.video.movies[0];
  favoriteShow = favorites.video.shows[0];
  favoriteMagazine = favorites.reading.magazines[0];
} catch (error) {
  favoriteMagazine = null;
  favoriteMovie = null;
  favoriteShow = null;
};

console.log(favoriteMovie); // null
console.log(favoriteShow); // null
console.log(favoriteMagazine); // null
Copy the code

If any of the attempts to get the properties fail, this causes them all to return their default values.

An alternative is to wrap the try with a reusable utility function… The catch.

const tryFn = (fn, fallback = null) => {
  try {
    return fn();
  } catch (error) {
    return fallback;
  }
} 

const favoriteBook = tryFn(() => favorites.reading.book[0]); // null
const favoriteMovie = tryFn(() => favorites.video.movies[0]); // "Casablanca"
Copy the code

Wrapping an object property in a function, you can defer the “unsafe” code and pass it to the try… The catch.

The main advantage of this approach is that it gets the properties quite naturally. As long as the property is encapsulated in a function, the property can be accessed securely and the specified default value can be returned for a nonexistent path.

Merge with the default object

By merging the object with a “default” object of similar structure, we can ensure that the path to the property is safe.

const defaults = {
  position: "static",
  background: "transparent",
  border: "none"}; const settings = { border:"1px solid blue"}; const merged = { ... defaults, ... settings }; console.log(merged); /* { position:"static",
    background: "transparent",
    border: "1px solid blue"} * /Copy the code

However, it is important to note that not a single attribute, but the entire nested object, is overridden.

const defaults = {
  font: {
    family: "Helvetica",
    size: "12px",
    style: "normal",
  },        
  color: "black"}; const settings = { font: { size:"16px",
  }
};

const merged = { 
  ...defaults, 
  ...settings,
};

console.log(merged.font.size); // "16px"
console.log(merged.font.style); // undefined
Copy the code

No! To solve this, we need to copy each nested object similarly.

const merged = { ... defaults, ... settings, font: { ... defaults.font, ... settings.font, }, }; console.log(merged.font.size); //"16px"
console.log(merged.font.style); // "normal"
Copy the code

Much better!

This pattern is common in such plug-ins or components that accept a large configurable object with default values.

An added benefit of this approach is that by writing a default object, we introduce documentation for that object. Unfortunately, copying each nested object for merge based on the size and structure of the data is potentially polluting.

Future: Optional chained calls

There is a feature in the current TC39 proposal called “optional chained calls”. The new operator looks like this:

console.log(favorites? .video? .shows[0]); //'The Simpsons'console.log(favorites? .audio? .audiobooks[0]); // undefinedCopy the code

? The. Operator works by short-circuiting: If? If the left side of the. Operator evaluates to null or undefined, the entire expression returns undefined and the right side is not evaluated.

In order to have a custom default value, we can use the | | operator to deal with undefined.

console.log(favorites? .audio? .audiobooks[0] ||"The Hobbit");
Copy the code

Which method should we use?

The answer, as you’ve probably guessed, is the old adage “it depends.” If optional chained calls have been added to the language and the necessary browser support is available, this may be the best option. However, if you’re not from the future, then you have more to think about. Are you using the library? How deep are your objects nested? Do you need to specify default values? We need to adopt different approaches according to different scenarios.

If you find any errors in the translation or other areas that need improvement, you are welcome to revise and PR the translation in the Gold Translation program, and you can also get corresponding bonus points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


Diggings translation project is a community for translating quality Internet technical articles from diggings English sharing articles. The content covers the fields of Android, iOS, front end, back end, blockchain, products, design, artificial intelligence and so on. For more high-quality translations, please keep paying attention to The Translation Project, official weibo and zhihu column.