I would like to know if JavaScript has "short-circuit" evaluation like &&-operator in C#. If not, I would like to know if there is a workaround that makes sense to adopt.
-
Also here an answer to my question developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…GibboK– GibboK2014-08-01 08:40:31 +00:00Commented Aug 1, 2014 at 8:40
-
Further useful resources: The || evaluation question The && evaluation questionSamuel Hulla– Samuel Hulla2019-05-09 11:08:26 +00:00Commented May 9, 2019 at 11:08
-
Closely related: Logical operators in JavaScript — how do you use them?.Sebastian Simon– Sebastian Simon2022-11-06 12:05:52 +00:00Commented Nov 6, 2022 at 12:05
3 Answers
Yes, JavaScript has "short-circuit" evaluation.
if (true || foo.foo){
// Passes, no errors because foo isn't defined.
}
if (false && foo.foo){
// Also passes, no errors because foo isn't defined.
}
11 Comments
Short-circuit with that logic operator. Just try it yourself. Use my demo.The answer is yes!
This answer goes into great detail on how short-circuiting works in JavaScript, with all the gotchas and also relevant themes such as operator precedence. If you’re looking for a quick definition and already understand how short-circuiting works, I’d recommend checking other answers.
What we (thought we) knew so far:
First let’s inspect the behavior we are all familiar with, inside the if condition, where we use && to check whether the two expressions are true:
const expr1 = true;
const expr2 = true;
if (expr1 && expr2) {
console.log("bar");
}
Now, your first instinct is probably to say: “Ah yes, quite simple, the code executes the statement if both expr1 and expr2 are evaluated as true“.
Well, yes and no. You are technically correct, that is the behavior you described, but that’s not exactly how the code is evaluated and we’ll need to delve deeper in order to fully understand.
How exactly is the && and || interpreted?
It’s time to look “under the hood” of the javascript engine. Let’s consider this practical example:
function sanitize(x) {
if (isNaN(x)) {
return NaN;
}
return x;
}
let userInput = 0xFF; // As an example.
const res = sanitize(userInput) && userInput + 5
console.log(res);
Well, the result is 260, but why?
In order to get the answer, we need to understand how the short-circuit evaluation works.
By the MDN definition, the && operator in expr1 && expr2 performs as follows:
Logical AND (
&&) evaluates operands from left to right, returning immediately with the value of the first falsy operand it encounters; if all values are truthy, the value of the last operand is returned.If a value can be converted to
true, the value is so-called truthy. If a value can be converted tofalse, the value is so-called falsy.[…]
As each operand is converted to a boolean, if the result of one conversion is found to be
false, the AND operator stops and returns the original value of that falsy operand; it does not evaluate any of the remaining operands.
Or, more simply, in an older revision of the documentation:
Operator Syntax Description Logical AND ( &&)expr1 && expr2If expr1can be converted totrue, returnsexpr2; else, returnsexpr1.
So this means, in our practical example, the const res is evaluated the following way:
- Invoke
expr1, which issanitize(userInput), orsanitize(0xFF). - Run
sanitize(0xFF): checkisNaN(x), orisNaN(0xFF), (which results infalsebecause0xFFis a valid hexadecimal number literal for 255), returnxwhich is0xFF, or255. IfisNaN(x)wastrue,sanitizewould’ve returnedNaN. - The
expr1resulted in255, a “truthy” value, so it’s time to evaluateexpr2. IfNaNwas returned, it’d stop asNaNis falsy. - Since
sanitize(userInput)is truthy (a non-zero, finite number), keep going and add5touserInput.
“Truthy” means that expression can be evaluated as true.
So here, we were able to avoid additional if blocks and further isNaN checks with a simple usage of the && operator.
How it really works:
By now, we should at least have a picture how the short-circuit operators work.
The operators that exhibit short-circuiting behavior are:
The universal rule goes:
a && b &&…&& zevaluates to the first falsy operand, or to the last operand if none is found to be falsy.a || b ||…|| zevaluates to the first truthy operand, or to the last operand if none is found to be truthy.a ?? b ??…?? zevaluates to the first operand which is neithernullnorundefined, or to the last operand, otherwise.a?.b?.…?.zaccesses each property in the chain and evaluates to the value of the nestedzproperty if all previous links of the chain evaluate to something that can be converted to an object (i.e. neithernullnorundefined); otherwise it returnsundefined, regardless of which nested property fails.
Here are some further examples for better comprehension:
function a() {
console.log("a");
return false;
}
function b() {
console.log("b");
return true;
}
if (a() && b()){
console.log("foobar");
}
// `a()` is evaluated as `false`; execution is stopped.
function a() {
console.log("a");
return false;
}
function b() {
console.log("b");
return true;
}
if (a() || b()){
console.log("foobar");
}
/*
** 1. `a()` is evaluated as `false`.
** 2. So it should evaluate `expr2`, which is `b()`.
** 3. `b()` is evaluated `true`.
** 4. The statement `console.log("foobar");` is executed.
*/
function a() {
console.log("a");
return null;
}
function b() {
console.log("b");
return false;
}
function c() {
console.log("c");
return true;
}
if (a() ?? b() ?? c()){
console.log("foobar");
}
/*
** 1. `a()` is evaluated as `null`.
** 2. So it should evaluate `expr2`, which is `b()`.
** 3. `b()` is evaluated as `false`; execution is stopped.
*/
const deeply = {
get nested(){
console.log("nested");
return {
get object(){
console.log("object");
return null;
}
};
}
};
if (deeply?.nested?.object?.but?.not?.that?.deep){
console.log("foobar");
}
/*
** 1. `deeply` is evaluated as an object.
** 2. `deeply?.nested` is evaluated as an object.
** 3. `deeply?.nested?.object` is evaluated as `null`.
** 4. `?.but?.not?.that?.deep` is essentially skipped over; the entire optional chain is evaluated as `undefined`; execution is stopped.
*/
One last pesky, but very important thing: Operator precedence
Nice, hopefully you’re getting the hang of it!
Last thing we need to know is a rule about operator precedence, that is: The && operator is always evaluated prior to the || operator.
Consider the following example:
function a() { console.log("a"); return true;}
function b() { console.log("b"); return false;}
function c() { console.log("c"); return false;}
console.log(a() || b() && c());
// "a" is logged; execution is stopped.
The expression a() || b() && c() will, perhaps confusingly to some, result in a().
The reason is quite simple, it’s just our eyesight that’s kind of deceiving us, because we’re used to reading left-to-right.
Let’s take the console.log() and what not out and focus purely on the evaluation:
true || false && false
&& having a higher precedence than || means that the operands closest to && are evaluated first — but after all operations with an even higher precedence already being evaluated.
Since there’re only && and || here, these are just false and false.
|| works the same way, including the rule that all operations with an even higher precedence should already be evaluated.
- So first, we need to evaluate
false && false, which is justfalse. - Then we evaluate
true || false(with the resultingfalse), which istrue. - The whole expression is equivalent to
(true || (false && false)), which is(true || (false)), which is(true).
You can also try an alternative perspective: in the ECMAScript specification (which is what the JavaScript language is based on), the expression expr1 || expr2 follows a pattern called the LogicalORExpression.
Following its definition, in simple terms, both expr1 and expr2 are their own LogicalANDExpression.
- If you wanted to evaluate something like
true || false && false, you’d have to evaluate the LogicalORExpression: (true)||(false && false). You know that (true) is justtrue, but you don’t immediately know what (false && false) is. - So then, you’d have to evaluate the LogicalANDExpression: (
false)&&(false). And now you’re done, because (false) is justfalse.
Only once you know the result of evaluating each LogicalANDExpression, you can move on to evaluate the constituting LogicalORExpression.
This is exactly what is meant by && being evaluated before ||, or && having a higher precedence than ||.
(NB: These grammar rules kill two birds with one stone: they define the evaluation order (via recursive definitions) and the operator precedence (in this case: left to right, and && before ||).)
Well, that might seem pretty tricky, all because of a few weird rules and semantics.
But remember, you can always escape operator precedence with parentheses, also known as the grouping operator (…) — just like in math.
function a() { console.log("a"); return true; }
function b() { console.log("b"); return false; }
function c() { console.log("c"); return false; }
console.log((a() || b()) && c());
/*
** 1. The parenthesized part is evaluated first.
** 2. `a()` is evaluated as `true`, so `b()` is skipped
** 3. `c()` is evaluated as `false`, stops execution.
*/
And we have yet to talk about where to place the ?? operator in terms of precedence!
But don’t worry: since operator precedence rules between && and || and ?? would be too confusing and too complicated, it is actually not allowed to put them next to each other!
They can only appear together in the same expression if it’s very clear which one is evaluated first.
(a ?? b) && c // Clearly, `(a ?? b)` is evaluated first.
a ?? (b && c) // Clearly, `(b && c)` is evaluated first.
a ?? b && c // Unclear! Throws a SyntaxError.
4 Comments
?.) and nullish-coalescing (??) operators! Honestly, this answer feels a bit much now, even if &&=, ||=, and ??= aren’t mentioned yet; maybe the detail about precedence isn’t needed that much.The idea is that logical expressions are read left-to-right, and if the value of the left condition is enough to get the total value, the right condition will not be processed and evaluated. Some very simple examples:
function test() {
const caseNumber = document.querySelector('#sel').value;
const userChoice = () => confirm('Press OK or Cancel');
if (caseNumber === '1') {
console.log (1 === 1 || userChoice());
} else if (caseNumber === '2') {
console.log (1 === 2 && userChoice());
} else if (caseNumber === '3') {
console.log (1 === 2 || userChoice());
} else if (caseNumber === '4') {
console.log (1 === 1 && userChoice());
} else if (caseNumber === '5') {
console.log (userChoice() || 1 === 1);
} else if (caseNumber === '6') {
console.log (userChoice() && 1 === 2);
}
}
<label for="sel">Select a number of a test case and press "RUN!":</label>
<br><select id="sel">
<option value="">Unselected</option>
<option value="1">Case 1</option>
<option value="2">Case 2</option>
<option value="3">Case 3</option>
<option value="4">Case 4</option>
<option value="5">Case 5</option>
<option value="6">Case 6</option>
</select>
<button onclick="test()">RUN!</button>
The first two cases above will print to console results true and false respectively and you will not even see the modal window asking you to press "OK" or "Cancel", because the left condition is sufficient to define the total result.
On the contrary, with the cases 3–6, you will see the modal window asking for your choice, because the former two depend on the right part (that is your choice), and the latter two — regardless of the fact that the aggregate values of these expressions do not depend on your choice — because left conditions are read first. So, it is important to place conditions left-to-right based on which ones you want to be processed first.