Home > other >  Optional chaining and await
Optional chaining and await

Time:11-08

Is there any reason to use if to check if method exists, before calling await

if (someObject.save){
  await someObject.save();
}

rather than using nullish coallescence diectly with await

 await someObject.save?.();

Is second code a safe alternative to the former?

CodePudding user response:

The best answer to this (I think) would be if you are working in a group where the group has decided not to use ?. because not everyone understands it. Otherwise, no. The second isn't safer

CodePudding user response:

It should be fine as you would await undefined.

Some considerations from the documentation:

const result = someInterface.customMethod?.();

if there is a property with such a name which is not a function, using ?. will still raise a TypeError exception "someInterface.customMethod is not a function".

Note: If someInterface itself is null or undefined, a TypeError exception will still be raised ("someInterface is null"). If you expect that someInterface itself may be null or undefined, you have to use ?. at this position as well: someInterface?.customMethod?.().

CodePudding user response:

Is second code a safe alternative to the former?

Yes. But maybe not for the reason one might expect.

The two pieces of code are equivalent in terms of the check made:

const obj = {
  foo() { console.log("method called"); },
  bar: 42
};

if (obj.foo) {
  console.log("obj has foo");
  obj.foo(); //works
}

console.log("using optional chaining");
obj.foo?.(); //works


if (obj.bar) {
  console.log("obj has bar");
  try {
    obj.bar(); //error
  } catch (err) {
    console.log("bar cannot be called");
  }
}

console.log("using optional chaining");
try {
  obj.bar?.() //error
} catch (err) {
  console.log("bar cannot be called");
}
.as-console-wrapper {
  max-height: 100% !important
}

However, in terms of async semantics, there is a difference: await will force an async function to pause. While not having await will run the function until another await is encountered or the function finishes and a result is produced.This can lead to very subtle bugs. Consider this case when the behaviour overlaps - there is a save() method, so both functions pause to await its result:

let x = 1;
async function testIf(someObject) {
  console.log("testIf 1:", x); //some external variable
  if (someObject.save){
    await someObject.save();
  }
  console.log("testIf 2:", x); //some external variable
}

async function testOptional(someObject) {
  console.log("testOptional 1:", x); //some external variable
  await someObject.save?.();
  console.log("testOptional 2:", x); //some external variable
}

const obj = { 
  save() { return Promise.resolve(); }
}
testIf(obj);
testOptional(obj);

x = 2; //change external variable

The behaviour is consistent: both of these will stop at the line with await, which will yield the execution, which then processes the new assignment to x, so then when both functions resume, they both read the new value of x. This is expected.

Now, consider what happens if the await line is not hit in the if version:

let x = 1;
async function testIf(someObject) {
  console.log("testIf 1:", x); //some external variable
  if (someObject.save){
    await someObject.save();
  }
  console.log("testIf 2:", x); //some external variable
}

async function testOptional(someObject) {
  console.log("testOptional 1:", x); //some external variable
  await someObject.save?.();
  console.log("testOptional 2:", x); //some external variable
}

const obj = {}
testIf(obj);
testOptional(obj);

x = 2;

The semantics changed. For the if version the await line is not processed, thus the execution does not yield until the function finishes, thus it reads the value of x before the reassignment both times. The version that uses optional chaining preserves its semantics even if save is not present - it still encounters the await and still yields.

In summary, having optionally asynchronous operation is the path to madness. I would suggest avoiding it at all costs.

Read more about this:

And yes, the code I showed is also probably bad - you most likely should not rely on external values. Yet, sometimes you have to. And even if you do not rely on them, in the future, somebody consuming your code might. It is best to just avoid Zalgo in the first place and have more predictable code.

  • Related