22  Logical Operators in Conditional Statements

NoteWhat this chapter covers

Real conditions are rarely a single comparison. You combine them, passes the exam and has 75% attendance, region is North or South, not failed. R provides the operators &, |, !, &&, ||, xor(), isTRUE(), and isFALSE() to build these compound conditions, plus %in% for set membership and short-circuit evaluation rules that affect both speed and safety. This chapter explains each operator, the crucial difference between the single and double forms, and how NA propagates through logical expressions.

22.1 The three building blocks

R has three classical logical operators:

Operator Name Meaning
&, && AND TRUE only if both sides are TRUE
\|, \|\| OR TRUE if either side is TRUE
! NOT flips TRUE ↔︎ FALSE

Everything else is built from these three.

22.2 Element-wise vs scalar, the & vs && rule

This is the single most important distinction in R logic.

  • & and | are element-wise: they compare two vectors position by position and return a vector the same length.
  • && and || are scalar: they expect a single TRUE/FALSE on each side and return a single value.

The scalar forms are designed for if statements, where the condition must collapse to one value:

WarningDon’t use && with vectors

In R 4.2 and later, passing a vector to && or || raises an error. Older code may “work” by silently using only the first element. Inside if (…), prefer &&/||. Outside if (…), i.e. when filtering vectors, use &/|.

# wrong shape for filtering - uses only first element
df[df$pass && df$paid, ]
# right
df[df$pass &  df$paid, ]

A simple rule: inside if(…), double; everywhere else, single.

22.3 Short-circuit evaluation

&& and || are short-circuit: they stop evaluating as soon as the answer is decided.

  • FALSE && anythingFALSE (right side never evaluated)
  • TRUE || anythingTRUE (right side never evaluated)

This matters when the right-hand side might error or is expensive to compute. The classic safety pattern:

If you wrote & instead of &&, R would try to evaluate every clause, including x[1] > 5 on a NULL, and you’d get unexpected behaviour. The element-wise & is not short-circuit.

22.4 NOT, the ! operator

! flips a logical vector. It is element-wise and works on any length.

Use ! to negate a condition: !is.na(x), !duplicated(x), !(region %in% c("North", "South")).

TipDe Morgan’s laws

Two identities worth memorising, they let you flip a complicated !(…) into something more readable:

  • !(a & b) is the same as !a | !b
  • !(a | b) is the same as !a & !b

So !(marks >= 50 & attendance >= 0.75) is equivalent to marks < 50 | attendance < 0.75. Choose the form that reads more naturally.

22.5 xor(), isTRUE(), isFALSE()

Three small helpers that come up often:

xor(a, b) is exclusive-or, TRUE when exactly one side is TRUE.

isTRUE(x) is TRUE only when x is exactly TRUE, not NA, not a vector, not a 1-length numeric. Useful when guarding against unknown input:

isFALSE() is the mirror image. Both are safer than raw == comparisons inside if, where any non-scalar or NA can wreck the condition.

22.6 NA in logical expressions

NA means “unknown.” When R combines an unknown with a known value, the answer is sometimes determined and sometimes not.

R follows what philosophers call three-valued logic: TRUE, FALSE, NA. The result is NA whenever the unknown could change the answer.

This bites in if statements:

Two defensive patterns:

22.7 %in%, set membership

%in% answers “is each element of x somewhere in y?” It is the cleanest replacement for chains of == joined by |.

%in% returns a logical vector of length length(x), never NA. To exclude a set, negate it: !(region %in% c("North","South")).

22.8 Operator precedence

Without brackets, R applies a fixed precedence: comparisons first, then !, then & / &&, then | / ||. The full chain is:

arithmetic   →   comparisons (<, ==, >=, %in%)   →   !   →   &, &&   →   |, ||

So marks >= 50 & marks <= 90 works as written, because >= and <= are evaluated before &. But mixing & and | without brackets is a recipe for confusion:

R reads & before |, so the condition becomes the first interpretation. Always parenthesise mixed &/| expressions, even when you remember the precedence, the next reader may not.

22.9 Worked example, exam eligibility

Decide whether each student in a class is eligible for the final exam. Rule: at least 50 marks and 75% attendance, or an exemption flag, but not if they are on academic hold.

Three things to notice:

  1. & and | (element-wise), we are filtering a vector, not running an if.
  2. The brackets make the precedence explicit, readers shouldn’t have to memorise the rule.
  3. !class$on_hold cleanly excludes anyone on hold, regardless of the other conditions.

22.10 Summary

Operator Element-wise Scalar NA-safe?
& / \| yes , propagates NA
&& / \|\| , yes (R ≥ 4.2 errors on vectors) propagates NA
! yes yes propagates NA
xor() yes yes propagates NA
%in% yes yes returns FALSE for NA
isTRUE() / isFALSE() , yes returns FALSE for NA

Two rules to live by:

  1. Inside if(…), use && and ||. Outside, use & and |.
  2. Always parenthesise mixed &/| expressions.

Logical operators are tiny but unforgiving, a misplaced & for &&, or a single missing pair of brackets, can quietly invert your analysis. Get into the habit of being explicit and your conditional code will read as clearly as your prose.