dplyr if_else() in R: Type-Strict Vectorized Conditional
The if_else() function in dplyr is a stricter, type-safer version of base R's ifelse(). It preserves Date / factor / POSIXct classes, errors on type mismatches, and accepts a separate missing argument for NA handling.
if_else(x > 0, "pos", "neg") # type-strict if_else(x > 0, x, -x) # absolute value, preserves type if_else(x > 0, x, NA_integer_) # NA_<type>_ for integer NA if_else(x > 0, "p", "n", missing = "?") # 3rd value for NAs if_else(date_x > today(), "future", "past") # preserves Date class ifelse(x > 0, "p", "n") # base R: lenient, may strip class
Need explanation? Read on for examples and pitfalls.
What if_else() does in one sentence
if_else(condition, true, false, missing) returns true where condition is TRUE, false where FALSE, and missing (default NA) where condition is NA. All three branches must be the SAME TYPE; mismatch errors instead of silently coercing.
This strictness is the main difference from base ifelse(). It catches type bugs early and preserves Date / factor classes.
Syntax
if_else(condition, true, false, missing = NA, ptype = NULL, size = NULL). condition, true, false must be vectors of the same length (or scalars).
dplyr::if_else() when working with Date / POSIXct / factor / Date columns. Base ifelse() strips class attributes, returning a numeric where you expected a Date.Five common patterns
1. Two-way recoding
2. Replace negatives with zero
The result is integer because both branches are integer. With base ifelse it would be double.
3. Explicit NA branch with missing
missing defaults to NA; specify it for an explicit third branch.
4. Preserve Date class
5. Combine with mutate in a pipeline
if_else() errors on type mismatch; base ifelse() silently coerces. if_else(x, 1L, "two") errors. ifelse(x, 1L, "two") returns a character vector. The strictness is a feature: it catches type bugs at the call site instead of letting them propagate.if_else() vs ifelse() vs case_when() vs switch()
Four conditional functions in R, with different strictness and scope.
| Function | Vectorized | Type-strict | NA arg | Best for |
|---|---|---|---|---|
dplyr::if_else() |
Yes | Yes | Yes (missing) |
Production pipelines |
base::ifelse() |
Yes | No (coerces) | No | Quick interactive |
dplyr::case_when() |
Yes | Yes | Implicit (TRUE branch) | 3+ outcomes |
base::switch() |
No (scalar) | Yes (per branch) | No | String dispatch |
When to use which:
if_elsefor 2-way branches in production code.ifelsefor quick scratch work where types don't matter.case_whenfor 3 or more branches.switchrarely; only for named-string scalar dispatch.
A practical workflow
The "type-preserving recode" pattern is the canonical if_else use case.
Each line is type-strict: types must match within branches. Date / factor / POSIXct columns survive intact.
Common pitfalls
Pitfall 1: type mismatch errors. if_else(x > 0, 1L, NA) errors because 1L is integer but NA is logical. Use NA_integer_ to force integer NA. Common typed NAs: NA_integer_, NA_real_, NA_character_.
Pitfall 2: condition must be logical. if_else(x, ...) errors if x isn't logical. Use if_else(x > 0, ...) or if_else(as.logical(x), ...).
if_else() requires true and false to have the SAME TYPE. if_else(x, "a", 1) errors. This catches bugs but means more typing (e.g., NA_integer_ instead of NA). Worth it for production code.Why type strictness matters
Silent type coercion is the #1 source of "why does my Date column look like a number?" bugs. Base ifelse() strips class attributes from the FALSE branch when types differ, turning Date into numeric, factor into integer, POSIXct into double. The fix is not "remember to call as.Date() after every ifelse"; it is to use if_else(), which errors on type mismatch. The minor inconvenience of typed NAs (NA_integer_, NA_Date_) is a small price for correctness. In a production pipeline, prefer if_else() everywhere; reserve base ifelse() for one-off interactive scripts where types are obvious.
Try it yourself
Try it: Create a derived column on mtcars with "efficient" if mpg >= 20, "thirsty" otherwise. Use if_else(). Save to ex_label.
Click to reveal solution
Explanation: if_else(mpg >= 20, "efficient", "thirsty") is type-strict (both branches character) and vectorized.
Related conditional functions
After mastering if_else, look at:
case_when(): 3+ branchescase_match(): switch-style value matchingcoalesce(): first non-NA across vectorsna_if(): convert specific value to NAdplyr::recode(): 1-to-1 value mappingcut(): bin numeric to intervals
For 3+ outcomes, case_when is dramatically more readable than nested if_else.
FAQ
What is the difference between if_else and ifelse in R?
dplyr::if_else() is type-strict (errors on mismatched branch types) and preserves classes (Date, factor, POSIXct). base::ifelse() is permissive (silently coerces) and strips classes.
Why does if_else error when ifelse worked?
Because if_else requires both branches to be the SAME TYPE. The error is intentional: it catches type bugs early.
How do I use NA in if_else?
Use a typed NA matching the other branch: NA_integer_, NA_real_, NA_character_, NA_Date_. if_else(x > 0, x, NA_real_) works; if_else(x > 0, x, NA) may not.
What is the missing argument in if_else?
missing is the value used when the condition is NA. Default is NA (matching the other branches' type). Set it explicitly for a 3-way branch including NAs.
Should I use if_else or case_when?
if_else for exactly 2 outcomes; case_when for 3 or more. case_when's vertical layout is much more readable than nested if_else.