179

In my code, I deal with an array that has some entries with many objects nested inside one another, whereas some do not. It looks something like the following:

// Where this array is hundreds of entries long, with a mix
// of the two examples given
var test = [{'a':{'b':{'c':"foo"}}}, {'a': "bar"}];

This is giving me problems, because I need to iterate through the array at times, and the inconsistency is throwing me errors like so:

for (i=0; i<test.length; i++) {
    // OK, on i==0, but 'cannot read property of undefined' on i==1
    console.log(a.b.c);
}

I am aware that I can say if(a.b){ console.log(a.b.c)}, but this is extraordinarily tedious in cases where there are up to 5 or 6 objects nested within one another. Is there any other (easier) way that I can have it only do the console.log if it exists, but without throwing an error?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ari
  • 3,489
  • 5
  • 26
  • 47
  • 4
    The error is probably a regular Javascript exception, so try the `try..catch` statement. That said, an array that contains wildly heterogenous elements looks like a design issue to me. – millimoose Feb 08 '13 at 22:17
  • 3
    If your structure isn't consistent across the items, then what's wrong with checking for existence? Really, I'd use `if ("b" in a && "c" in a.b)`. It may be "tedious", but that's what you get for inconsistency...normal logic. – Ian Feb 08 '13 at 22:21
  • 2
    Why would you access non-existing properties, why don't you know how the objects look like? – Bergi Feb 08 '13 at 22:22
  • 10
    I can understand why somebody would not want an error to crash everything. You can't always rely on an object's properties to exist or not exist. If you have something in place that can handle the event that the object is malformed, then you're code is much more efficient and less fragile. – SSH This Apr 09 '13 at 20:19
  • 1
    I completely agree. It's obviously a debated topic, but I honestly don't understand why languages' default behavior is to crash. 99% of the time the bug fix is to insert a check. I want `a.b.c.d` to result in null if *any* of a, b, c *or* d is null, much like how Objective-C treats any message sent to nil as a no-op. It's so much nicer. – devios1 Sep 14 '16 at 17:45
  • 3
    You'd be surprised at how many objects/arrays are malformed in real life situations – OneMoreQuestion Sep 08 '17 at 02:42
  • @Bergi, one of the cases when you work with a framework that has ORM and you retrieve data of a model that has related models. Sometimes one of the related models does not exist. So you cannot access its properties. It is not about accessing non-existing properties but accessing properties of possible non-existing object. – O Connor May 29 '20 at 09:17

18 Answers18

228

Update:

  • If you use JavaScript according to ECMAScript 2020 or later, see optional chaining.
  • TypeScript has added support for optional chaining in version 3.7.
// use it like this
obj?.a?.lot?.of?.properties

Solution for JavaScript before ECMASCript 2020 or TypeScript older than version 3.7:

A quick workaround is using a try/catch helper function with ES6 arrow function:

function getSafe(fn, defaultVal) {
  try {
    return fn();
  } catch (e) {
    return defaultVal;
  }
}

// use it like this
console.log(getSafe(() => obj.a.lot.of.properties));

// or add an optional default value
console.log(getSafe(() => obj.a.lot.of.properties, 'nothing'));
str
  • 42,689
  • 17
  • 109
  • 127
  • 2
    I love it! the only thing I would add is a console.warn inside the catch, so that you know of the error but it continues on. – Rabbi Shuki Gur Dec 11 '18 at 10:31
  • 3
    **Catching *all* exceptions without re-throwing is bad**, and generally using exceptions as part of the expected flow of execution is also not great -- even though in this case it's pretty well contained. – hugo Aug 25 '19 at 11:28
  • Oh man, you're my hero now!! try and catch did it for me after all my failed efforts with Object.assign, 'OR' operator, ternary operator, etc nothing works in the presence of an undefined error. Thank you!! –  Feb 11 '22 at 15:11
  • beautiful operator .? - in python we dont have it and must use lodash package – Nam G VU Aug 18 '23 at 10:14
55

What you are doing raises an exception (and rightfully so). You can always do:

try{
   window.a.b.c
}catch(e){
   console.log("YO",e)
}

But I wouldn't, instead think of your use case.

Why are you accessing data, 6 levels nested that you are unfamiliar of? What use case justifies this?

Usually, you'd like to actually validate what sort of object you're dealing with.

Also, on a side note you should not use statements like if(a.b) because it will return false if a.b is 0 or even if it is "0". Instead check if a.b !== undefined

Youssouf Oumar
  • 29,373
  • 11
  • 46
  • 65
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 2
    In regards to your first edit: It is justified; I am dealing with JSON structured database entries, such that the objects will scale multiple levels of fields (ie. entries.users.messages.date etc., where not all cases have data entered) – Ari Feb 08 '13 at 22:23
  • "it will return true if a.b is 0" - nope. `typeof a.b === "undefined" && a.b!=null` - unnecessary to do the second part after the first, and it makes more sense to just do `if ("b" in a)` – Ian Feb 08 '13 at 22:23
  • @Ian yeah, I obviously meant it the other way around, it will return false even if a.b is "0". Nice catch – Benjamin Gruenbaum Feb 08 '13 at 22:24
  • @BenjaminGruenbaum Sounds good, wasn't sure if you meant that. Also, I think you want `typeof a.b !`== "undefined" && a.b!=null` - notice the `!==` – Ian Feb 08 '13 at 22:26
  • 3
    If you don't want to tedium of a.b && a.b.c && console.log(a.b.c) then this is the only way to consistently log unknowns. – Brian Cray Feb 08 '13 at 22:31
  • Use `console.log(e.message)` to show simple error message. – Nick Tsai Jul 21 '17 at 02:30
15

If I am understanding your question correctly, you want the safest way to determine if an object contains a property.

The easiest way is to use the in operator.

window.a = "aString";
//window should have 'a' property
//lets test if it exists
if ("a" in window){
    //true
 }

if ("b" in window){
     //false
 }

Of course you can nest this as deep as you want

if ("a" in window.b.c) { }

Not sure if this helps.

Edric
  • 24,639
  • 13
  • 81
  • 91
matt weiss
  • 345
  • 1
  • 4
  • 13
    You can't safely nest this as deeply as you want. What if `window.b` is undefined? You will get a type error: `Cannot use 'in' operator to search for 'c' in undefined` – Trevor Aug 28 '17 at 23:34
14

If you are using lodash, you could use their has function. It is similar to the native "in", but allows paths.

var testObject = {a: {b: {c: 'walrus'}}};
if(_.has(testObject, 'a.b.c')) {
  //Safely access your walrus here
}
Youssouf Oumar
  • 29,373
  • 11
  • 46
  • 65
tehwalris
  • 404
  • 5
  • 10
  • 3
    Best, we can use `_.get()` with default for easy read : `_.get(object, 'a.b.c', 'default');` – Ifnot Jun 18 '18 at 13:53
14

Try this. If a.b is undefined, it will leave the if statement without any exception.

if (a.b && a.b.c) {
  console.log(a.b.c);
}
Youssouf Oumar
  • 29,373
  • 11
  • 46
  • 65
Sean Chen
  • 239
  • 3
  • 3
7

If you use Babel, you can already use the optional chaining syntax with @babel/plugin-proposal-optional-chaining Babel plugin. This would allow you to replace this:

console.log(a && a.b && a.b.c);

with this:

console.log(a?.b?.c);
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
rishat
  • 8,206
  • 4
  • 44
  • 69
6

If you have Lodash, you can use its .get method:

_.get(a, 'b.c.d.e')

Or give it a default value:

_.get(a, 'b.c.d.e', default)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Brandon Dyer
  • 1,316
  • 12
  • 21
5

I use undefsafe religiously. It tests each level down into your object until it either gets the value you asked for, or it returns "undefined". But never errors.

martinedwards
  • 5,577
  • 1
  • 33
  • 35
5

This is a common issue when working with deep or complex JSON object, so I try to avoid try/catch or embedding multiple checks which would make the code unreadable. I usually use this little piece of code in all my projects to do the job.

/* Example: getProperty(myObj, 'aze.xyz', 0) // return myObj.aze.xyz safely
 * accepts array for property names:
 *     getProperty(myObj, ['aze', 'xyz'], {value: null})
 */
function getProperty(obj, props, defaultValue) {
    var res, isvoid = function(x) {return typeof x === "undefined" || x === null;}
    if(!isvoid(obj)) {
        if(isvoid(props))
            props = [];
        if(typeof props  === "string")
            props = props.trim().split(".");
        if(props.constructor === Array) {
            res = props.length>1 ? getProperty(obj[props.shift()], props, defaultValue) : obj[props[0]];
        }
    }
    return typeof res === "undefined" ? defaultValue: res;
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Maelkhor
  • 51
  • 1
  • 1
4

I like Cao Shouguang's answer, but I am not fond of passing a function as parameter into the getSafe function each time I do the call. I have modified the getSafe function to accept simple parameters and pure ES5.

/**
* Safely get object properties.
* @param {*} prop The property of the object to retrieve
* @param {*} defaultVal The value returned if the property value does not exist
* @returns If property of object exists it is returned,
*          else the default value is returned.
* @example
* var myObj = {a : {b : 'c'} };
* var value;
*
* value = getSafe(myObj.a.b,'No Value'); //returns c
* value = getSafe(myObj.a.x,'No Value'); //returns 'No Value'
*
* if (getSafe(myObj.a.x, false)){
*   console.log('Found')
* } else {
*  console.log('Not Found')
* }; //logs 'Not Found'
*
* if(value = getSafe(myObj.a.b, false)){
*  console.log('New Value is', value); //logs 'New Value is c'
* }
*/
function getSafe(prop, defaultVal) {
  return function(fn, defaultVal) {
    try {
      if (fn() === undefined) {
        return defaultVal;
      } else {
        return fn();
      }
    } catch (e) {
      return defaultVal;
    }
  }(function() {return prop}, defaultVal);
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Hardy Le Roux
  • 1,489
  • 4
  • 17
  • 28
3

Lodash has a get method which allows for a default as an optional third parameter, as show below:

const myObject = {
  has: 'some',
  missing: {
    vars: true
  }
}
const path = 'missing.const.value';
const myValue = _.get(myObject, path, 'default');
console.log(myValue) // prints out default, which is specified above
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
AncientSwordRage
  • 7,086
  • 19
  • 90
  • 173
2

In str's answer, value 'undefined' will be returned instead of the set default value if the property is undefined. This sometimes can cause bugs. The following will make sure the defaultVal will always be returned when either the property or the object is undefined.

const temp = {};
console.log(getSafe(()=>temp.prop, '0'));

function getSafe(fn, defaultVal) {
    try {
        if (fn() === undefined || fn() === null) {
            return defaultVal
        } else {
            return fn();
        }

    } catch (e) {
        return defaultVal;
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • Hardy Le Roux's improved version of my code doesn't work with let myObj ={} getSafe(()=>myObj.a.b, "nice"), while mine works. Anyone explains why? – Cao Shouguang Jul 23 '20 at 01:13
2

Imagine that we want to apply a series of functions to x if and only if x is non-null:

if (x !== null) x = a(x);
if (x !== null) x = b(x);
if (x !== null) x = c(x);

Now let's say that we need to do the same to y:

if (y !== null) y = a(y);
if (y !== null) y = b(y);
if (y !== null) y = c(y);

And the same to z:

if (z !== null) z = a(z);
if (z !== null) z = b(z);
if (z !== null) z = c(z);

As you can see without a proper abstraction, we'll end up duplicating code over and over again. Such an abstraction already exists: the Maybe monad.

The Maybe monad holds both a value and a computational context:

  1. The monad keeps the value safe and applies functions to it.
  2. The computational context is a null check before applying a function.

A naive implementation would look like this:

⚠️ This implementation is for illustration purpose only! This is not how it should be done and is wrong at many levels. However this should give you a better idea of what I am talking about.

As you can see nothing can break:

  1. We apply a series of functions to our value
  2. If at any point, the value becomes null (or undefined) we just don't apply any function anymore.

const abc = obj =>
  Maybe
    .of(obj)
    .map(o => o.a)
    .map(o => o.b)
    .map(o => o.c)
    .value;

const values = [
  {},
  {a: {}},
  {a: {b: {}}},
  {a: {b: {c: 42}}}
];

console.log(

  values.map(abc)

);
<script>
function Maybe(x) {
  this.value = x; //-> container for our value
}

Maybe.of = x => new Maybe(x);

Maybe.prototype.map = function (fn) {
  if (this.value == null) { //-> computational context
    return this;
  }
  return Maybe.of(fn(this.value));
};
</script>

Appendix 1

I cannot explain what monads are as this is not the purpose of this post and there are people out there better at this than I am. However as Eric Elliot said in hist blog post JavaScript Monads Made Simple:

Regardless of your skill level or understanding of category theory, using monads makes your code easier to work with. Failing to take advantage of monads may make your code harder to work with (e.g., callback hell, nested conditional branches, more verbosity).


Appendix 2

Here's how I'd solve your issue using the Maybe monad from

const prop = key => obj => Maybe.fromNull(obj[key]);

const abc = obj =>
  Maybe
    .fromNull(obj)
    .flatMap(prop('a'))
    .flatMap(prop('b'))
    .flatMap(prop('c'))
    .orSome('')
    
const values = [
  {},
  {a: {}},
  {a: {b: {}}},
  {a: {b: {c: 42}}}
];

console.log(

  values.map(abc)

);
<script src="https://www.unpkg.com/monet@0.9.0/dist/monet.js"></script>
<script>const {Maybe} = Monet;</script>
customcommander
  • 17,580
  • 5
  • 58
  • 84
2

You can use optional chaining from the ECMAScript standard. Like this:

a?.b?.c?.d?.func?.()
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
0

I answered this before and happened to be doing a similar check today. A simplification to check if a nested dotted property exists. You could modify this to return the value, or some default to accomplish your goal.

function containsProperty(instance, propertyName) {
    // make an array of properties to walk through because propertyName can be nested
    // ex "test.test2.test.test"
    let walkArr = propertyName.indexOf('.') > 0 ? propertyName.split('.') : [propertyName];

    // walk the tree - if any property does not exist then return false
    for (let treeDepth = 0, maxDepth = walkArr.length; treeDepth < maxDepth; treeDepth++) {

        // property does not exist
        if (!Object.prototype.hasOwnProperty.call(instance, walkArr[treeDepth])) {
            return false;
        }

        // does it exist - reassign the leaf
        instance = instance[walkArr[treeDepth]];

    }

    // default
    return true;

}

In your question you could do something like:

let test = [{'a':{'b':{'c':"foo"}}}, {'a': "bar"}];
containsProperty(test[0], 'a.b.c');
matt weiss
  • 345
  • 1
  • 4
0

I usually use something like this:

 var x = object.any ? object.any.a : 'def';
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Vansuita Jr.
  • 1,949
  • 17
  • 18
0

You can avoid getting an error by giving a default value before getting the property

var test = [{'a':{'b':{'c':"foo"}}}, {'a': "bar"}];

for (i=0; i<test.length; i++) {
    const obj = test[i]
    // No error, just undefined, which is ok
    console.log(((obj.a || {}).b || {}).c);
}

This works great with arrays too:

const entries = [{id: 1, name: 'Scarllet'}]
// Giving a default name when is empty
const name = (entries.find(v => v.id === 100) || []).name || 'no-name'
console.log(name)
Igor Parra
  • 10,214
  • 10
  • 69
  • 101
0

Unrelated to the question's actual question, but might be useful for people coming to this question looking for answers.

Check your function parameters.

If you have a function like const x({ a }) => { }, and you call it without arguments x(); append = {} to the parameter: const x({ a } = {}) => { }.


What I had

I had a function like this:

const x = ({ a }) => console.log(a);
// This one works as expected
x({ a: 1 });
// This one errors out
x();

Which results in "Uncaught TypeError: Cannot destructure property 'a' of 'undefined' as it is undefined."


What I switched it to (now works).

const x = ({ a } = {}) => console.log(a);
// This one works as expected
x({ a: 1 });
// This now works too!
x();
ctwheels
  • 21,901
  • 9
  • 42
  • 77