Written and maintained by CASRAI Editorial Board
Last updated
case_when() is the dplyr function for recoding a column by several conditions at once: it checks each condition in order and returns the value attached to the first one that is true. The details that cause wrong output in practice are the order of conditions, how NA is handled, what .default and .unmatched do, and the type rules for the returned values. This guide covers the syntax and then those four traps, with every behaviour taken from the dplyr reference pages (checked 2026-10-29).
What case_when() is used for in research
In an analysis script, case_when() turns a raw variable into an analysis-ready one: grouping a continuous age into bands, collapsing survey response codes into categories, flagging records that meet an inclusion rule, or labelling values that fall outside a threshold. It is the vectorised form of an if-else chain, so one call recodes a whole column. It is a dplyr function, not part of base R. For a plain two-way split, see the companion guide on ifelse in R, which also touches on case_when() for several categories.
Install or access
Install dplyr from CRAN, either on its own or as part of the tidyverse, then load it:
install.packages("dplyr") # or install.packages("tidyverse")
library(dplyr)
The dplyr documentation lists both install commands on its home page. The function reference is at dplyr.tidyverse.org/reference/case-and-replace-when.html.
Step-by-step: recode a numeric column into groups
The signature, as documented, is case_when(..., .default = NULL, .unmatched = "default", .ptype = NULL, .size = NULL). Each argument in ... is a two-sided formula: a logical condition on the left of ~, the value to return on the right.
- Load dplyr with
library(dplyr). - Write conditions from most specific to most general. Arguments are evaluated in order and the first match wins.
- Put the formulas inside
mutate()to create the new column. - Add a
.defaultfor rows no condition matches.
An illustrative example with a made-up data frame called df holding an age column:
df |> mutate(
age_group = case_when(
is.na(age) ~ "unknown",
age < 18 ~ "under 18",
age < 65 ~ "18 to 64",
.default = "65 and over"
)
)
Because the first true condition wins, an age of 10 is labelled “under 18” even though age < 65 is also true. The explicit is.na(age) case comes first because a condition that evaluates to NA is treated like FALSE; without it, rows with a missing age would fall through to .default and be labelled “65 and over”. That is covered in the troubleshooting section below.
Output and how to read it
The result is a vector with one value per row, the same length as the logical conditions. The right-hand values are cast to a common type and recycled to that length, and any NULL inputs are ignored. If no condition is true for a row and no .default is given, that row is NA, because NULL as the default gives NA.
Troubleshooting
Why do my conditions give the wrong label? Order of conditions
Conditions are checked in order, so an overly general condition placed first shadows every later one. The dplyr reference shows this with x <- 1:70:
case_when(
x %% 5 == 0 ~ "fizz",
x %% 7 == 0 ~ "buzz",
x %% 35 == 0 ~ "fizz buzz",
.default = as.character(x)
)
The first values returned are “1” “2” “3” “4” “fizz” “6” “buzz” “8” “9”, and “fizz buzz” never appears, because multiples of 35 are already caught by the first two conditions. Put the most specific condition (x %% 35 == 0) first.
How does case_when() treat NA?
A condition that evaluates to NA is treated like FALSE, so those rows fall through to .default. If missing values need their own label, add an explicit is.na(x) ~ value case before the default. With no default and no match, the result is NA: the reference example case_when(x %% 35 == 0 ~ "fizz buzz", x %% 5 == 0 ~ "fizz", x %% 7 == 0 ~ "buzz") on 1:70 begins NA NA NA NA “fizz”. For general missing-data handling, see omitting rows with NA in R and replacing NA with 0 in R.
How do I make unmatched rows an error? The .unmatched argument
.default supplies the value where every condition is FALSE or NA. Setting .unmatched = "error" instead stops the call if any row is unmatched, which is a useful guard when the conditions are meant to cover every case. The reference example, run on set.seed(123); x <- sample(50), reports “Each location must be matched.” and “Location 31 is unmatched.” With .unmatched = "error", an NA condition counts as unmatched, so missing values need their own case.
Why do I get “Can’t combine” type errors?
The right-hand values must be combinable into one type. The reference shows case_when(is.character(x) ~ x, .default = "not-a-character") with x <- 1 failing with: Can’t combine <double> and `.default` <character>. Make every right-hand side, including .default, the same type, for example by wrapping numbers in as.character() as in the example above.
Why do I get NaN warnings from a branch that should not run?
All right-hand expressions are evaluated first and the matching values are selected afterwards. The reference gives sqrt(y) producing NaN warnings for negative y even when a condition routes those rows elsewhere. For a single true/false decision, a scalar if/else is the documented better choice because it is faster and lazier.
Recoding part of a column without changing its type
case_when() combines the right-hand values into a common type, so recoding a factor with a character .default returns a character vector. The same reference page documents replace_when(x, ...), which keeps the type of x so factors stay factors. It has no .default, .unmatched, .ptype or .size arguments and is meant for partially updating an existing vector.
Free alternatives
| Function | Use it when | Source note |
|---|---|---|
case_when() |
Several conditions, new vector built from the values you give | dplyr reference, case_when() |
replace_when() |
Updating only some elements of an existing vector, keeping its type | dplyr reference, replace_when() |
case_match() |
Matching a vector against sets of old values, for example c(1, 3) ~ "odd" |
dplyr reference; deprecated in dplyr 1.2.0, which points to recode_values() and replace_values() |
ifelse() and if_else() |
A single two-way split | See ifelse in R |
The case_match() reference page notes that typeof(case_match(y, NA ~ 0, .default = y)) is “double”, whereas typeof(replace_values(y, NA ~ 0)) is “integer”.
FAQ
Is case_when() in base R?
No. It comes from the dplyr package, so load it with library(dplyr) first.
What does .default do in case_when()?
It is the value returned where every condition is FALSE or NA. If it is left as NULL, those rows are NA.
Which condition wins when several are true?
The first one, because arguments are evaluated in order. List the most specific conditions first.
Is case_match() still recommended?
The dplyr reference marks case_match() as deprecated in dplyr 1.2.0 and recommends recode_values() and replace_values(). See the dplyr documentation for those functions before using them.
Related: summary statistics in R for summarising the recoded groups.
Sources
- dplyr reference, A general vectorised if-else (case_when and replace_when), checked 2026-10-29.
- dplyr reference, case_match(), checked 2026-10-29.
- dplyr home page (installation commands), checked 2026-10-29.







