R PROGRAMMING • CONTROL FLOW

If/Else & Else-If Chains — Write if/else statements and else-if chains

Master conditional branching in R to direct program execution along distinct logical paths.

Historical Context & Motivation

The ability to choose between different execution paths based on a condition is one of the most fundamental ideas in computing. Before high-level languages offered elegant conditional branching syntax, early programmers relied on machine-level jump instructions to redirect the flow of their programs. The conceptual leap from raw branch instructions to structured if/else statements transformed programming from an error-prone exercise in address manipulation into a disciplined, readable craft. R inherits this tradition through a lineage that stretches from ALGOL through S and S-Plus, giving statisticians and data scientists the same expressive conditional logic available in general-purpose languages.

1958
ALGOL Introduces Structured Conditionals
The ALGOL 58 specification formalized the if … then … else construct, establishing the template that nearly every imperative language would follow for decades.
1976
The S Language at Bell Labs
John Chambers and colleagues created S, a language for statistical computing that included if/else control flow alongside vectorized operations, bridging the gap between programming and data analysis.
1993
R Is Born
Ross Ihaka and Robert Gentleman released R as a free implementation of the S language. R preserved S-style conditionals while adding its own idioms, such as ifelse() for vectorized conditional evaluation.
2000s
Tidyverse & Modern R Idioms
The rise of the tidyverse introduced dplyr::case_when() as a vectorized alternative to long else-if chains, yet the core if/else construct remains indispensable for scalar control flow in functions and scripts.

The central question that conditional statements answer is deceptively simple: given that a certain condition is either true or false, which block of code should the program execute next? When multiple conditions need to be evaluated in sequence, else-if chains provide a disciplined way to express multi-branch logic without deeply nested structures. Understanding these constructs in R is essential before tackling loops, function design, and reactive programming in Shiny applications.

Core Principles & Definitions

Before writing any conditional code, it is important to internalize the foundational rules that govern how R evaluates conditions and selects execution paths. R's if statement expects a single logical value — TRUE or FALSE — and will issue a warning if you supply a vector of length greater than one. This scalar requirement is a deliberate design choice that distinguishes the control-flow if from the vectorized ifelse() function, a distinction that trips up many newcomers.

1

Condition Must Be Scalar

The expression inside if() must evaluate to a single TRUE or FALSE. Passing a logical vector of length > 1 triggers a warning and uses only the first element.
2

Braces Define Blocks

Curly braces { } delimit the body of each branch. Although R allows omitting braces for single-expression bodies, best practice is always to include them for clarity and to avoid subtle bugs.
3

else Must Follow Immediately

In R, the else keyword must appear on the same line as the closing brace of the preceding if or else if block — otherwise R's parser treats the if as a complete statement.
4

Else-If Chains Are Sequential

Conditions in an else if chain are evaluated top-to-bottom. The first condition that evaluates to TRUE has its block executed; all subsequent branches are skipped entirely.
5

Conditionals Return Values

In R, if/else is an expression, not just a statement. The entire construct returns the value of whichever branch was executed, enabling assignment patterns like x <- if (cond) a else b.
KEY TAKEAWAY
Think of an if/else chain like a series of locked doors in a hallway. You walk up to the first door and test the key (the condition). If it opens (TRUE), you walk through and ignore every remaining door. If not, you move to the next door and try again. The final else is the unlocked door at the end — the default path when no key worked.

Visual Explanation — Control Flow Diagram

The flowchart shows an else-if chain with two conditions and a default else block. Diamond nodes represent conditions; rectangles represent code blocks. A TRUE result branches right into the corresponding block, while FALSE falls through to the next condition. All branches ultimately converge at CONTINUE.

In the diagram above, notice that exactly one rectangular code block is executed per traversal of the chain. This mutual exclusivity is a defining characteristic of else-if chains: once a branch fires, no further conditions are tested. Contrast this with a sequence of independent if statements without else, where multiple blocks could execute if multiple conditions happen to be true. Understanding this distinction is critical when conditions overlap.

How It Works — Syntax & Evaluation Rules

Basic if Statement

The simplest conditional in R is a standalone if block. The syntax is: if (condition) { body }. The condition is any R expression that evaluates to a scalar logical value. If TRUE, the body executes; if FALSE, R silently skips the block and returns NULL invisibly. Numeric values are coerced: zero becomes FALSE and any non-zero value becomes TRUE.

SIMPLE IF
if (x > 0) { result <- "positive" }
x > 0 is the condition — a relational expression yielding TRUE or FALSE. The assignment inside the braces executes only when the condition is TRUE.

if/else Statement

Adding an else clause ensures that one of two blocks always executes. This is the canonical two-way branch. A critical R-specific detail: the else keyword must appear on the same line as the closing brace of the if block. Placing it on a new line causes R's parser to interpret the if block as complete, resulting in an "unexpected 'else'" error.

IF / ELSE
if (x > 0) { result <- "positive" } else { result <- "non-positive" }
The closing } and else share a line. This guarantees R's parser treats them as a single compound expression.

Else-If Chains

When you need more than two branches, chain together else if clauses. Each clause introduces a new condition that is evaluated only if all preceding conditions were FALSE. The final else (without a condition) is optional but strongly recommended; it serves as the default case and prevents the expression from returning NULL when no condition matches.

ELSE-IF CHAIN
if (score >= 90) { grade <- "A" } else if (score >= 80) { grade <- "B" } else if (score >= 70) { grade <- "C" } else { grade <- "F" }
Conditions are tested top-to-bottom. A score of 85 matches score >= 80 and also score >= 70, but only the first matching branch ("B") executes.
⚠️ Common Pitfall: else on a New Line
In interactive R sessions and scripts, placing else on its own line after the closing } causes the error: Error: unexpected 'else' in "else". Always write } else { on one line. Inside function bodies or source()-d files the parser is more lenient, but the one-line style is universally safe.

Common Patterns & Idiomatic R Conditionals

Beyond the basic syntax, experienced R programmers employ several idiomatic patterns that leverage the fact that if/else is an expression that returns a value. This section catalogs the most frequently encountered patterns and contrasts the scalar if/else with the vectorized ifelse() function and dplyr::case_when().

A side-by-side comparison of R's three primary conditional mechanisms. The scalar if/else (left) is ideal for control flow inside functions; the vectorized ifelse() (center) operates element-wise on vectors; case_when() (right) extends vectorized branching to arbitrarily many conditions.

Pattern 1: Inline Assignment

Because R's if/else construct is an expression that returns the value of the executed branch, you can assign the result directly to a variable: msg <- if (n %% 2 == 0) "even" else "odd". This pattern reduces repetitive assignment statements and is especially concise for binary conditions. For chains with more than two outcomes, the pattern extends naturally with else if clauses, though at that point readability may benefit from a separate helper function or switch().

Pattern 2: Guard Clauses

A guard clause is a standalone if block placed at the top of a function to validate inputs or handle edge cases early. When the guard condition is met, the function typically calls stop(), warning(), or return() to exit immediately. This keeps the main logic at a low indentation level and improves readability, a practice borrowed from defensive programming in software engineering.

Pattern 3: Nested Conditionals (and When to Avoid Them)

You can nest if/else statements to express compound decisions, but deep nesting rapidly degrades readability. As a rule of thumb, if you find yourself nesting more than two levels, consider refactoring: extract the inner logic into a named helper function, or flatten the nesting into an else-if chain with combined Boolean conditions using && (short-circuit AND) and || (short-circuit OR). Note that && and || evaluate only the first element of each operand, making them the correct choice inside if conditions, whereas & and | are element-wise and intended for vectorized operations.

Worked Example — Grade Classification Function

Let us build a function classify_bmi() that takes a numeric BMI value and returns a character string indicating the WHO weight-status category. This example exercises the full else-if chain pattern, includes a guard clause for input validation, and demonstrates the expression-return idiom.

Building classify_bmi() Step by Step
1
Step 1 — Define the Function SignatureWe declare a function that accepts a single numeric argument bmi. The function will return a character string. In R: classify_bmi <- function(bmi) { # body goes here }
2
Step 2 — Add a Guard ClauseBefore any branching logic, validate the input. If bmi is not a single positive number, we stop execution with an informative error message: if (!is.numeric(bmi) || length(bmi) != 1 || bmi <= 0) { stop("bmi must be a single positive number.") }
Invalid inputs are caught immediately, preventing misleading return values.
3
Step 3 — Write the Else-If ChainUsing WHO cut-off values, we build a four-branch chain. Each condition is tested only if all prior conditions were FALSE, so we can use simple greater-than-or-equal comparisons without upper-bound checks: category <- if (bmi < 18.5) { "Underweight" } else if (bmi < 25) { "Normal weight" } else if (bmi < 30) { "Overweight" } else { "Obese" }
The entire if/else expression returns one of four strings, assigned to category.
4
Step 4 — Return the ResultIn R, the last evaluated expression in a function is returned implicitly. We place category as the final line, or equivalently wrap it in return(category) for explicitness.
5
Step 5 — Test the FunctionWe call the function with several test values to verify correctness: classify_bmi(22.1) # "Normal weight" classify_bmi(17.0) # "Underweight" classify_bmi(28.5) # "Overweight" classify_bmi(35.0) # "Obese"
All test cases return the expected WHO category. Passing classify_bmi(-5) triggers the guard clause error, confirming input validation works.

Strengths, Limitations & When to Choose Alternatives

The scalar if/else construct is the backbone of R's control flow, but it is not always the best tool. The table below compares it against alternatives across several dimensions, helping you make informed choices in real code.

Comparison of R conditional mechanisms
Featureif / else if / elseswitch()ifelse() / case_when()
Input typeScalar logicalScalar character or integerLogical vector
Number of branchesArbitrary (chain)Arbitrary (named cases)2 for ifelse(); N for case_when()
VectorizedNoNoYes
Returns valueYesYesYes
Best use caseGeneral control flow with complex conditionsDispatching on exact string/integer matchesColumn-wise transformations on data frames
Readability at many branchesDecreases (long chains)Excellent (named cases)Excellent (case_when formulas)
KEY TAKEAWAY
Think of if/else as a traffic officer directing a single car (scalar) through an intersection, switch() as a menu selector that jumps directly to a named destination, and ifelse()/case_when() as a sorting machine that classifies every item on a conveyor belt simultaneously. Choose the right tool for the granularity of your problem.

Connection to Advanced Control Flow

Mastering if/else chains lays the groundwork for more sophisticated control-flow and dispatch mechanisms in R. The table below maps concepts from this lesson to their advanced counterparts. You will encounter these as you progress into package development, functional programming with purrr, and reactive programming with Shiny.

From basic conditionals to advanced R patterns
This LessonAdvanced VersionWhen You'll Need It
if/else chainswitch() with named casesDispatching on a string argument inside a function (e.g., method = "lm" vs. "glm")
Nested if statementsS3/S4 method dispatch (UseMethod())Behavior that varies by object class, central to object-oriented R programming
Guard clauses with stop()tryCatch() / withCallingHandlers()Structured error handling and recovery in production-grade R code
Inline if/else expressionClosures capturing conditional logicFactory functions that return different functions based on configuration parameters
if/else in reactive contextreq(), observeEvent(), conditionalPanel()Building interactive Shiny dashboards with conditional UI rendering

A particularly important transition occurs when you move from scalar control flow to vectorized conditional logic. In data-analysis workflows, you will often need to apply a classification rule to every row of a data frame. Attempting to use a scalar if/else inside dplyr::mutate() is a common error that produces either warnings or incorrect results. The correct approach is to use case_when() or ifelse() for vectorized contexts, while reserving if/else for single-value decisions in functions and scripts.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain why the following R code produces an error, and describe precisely how to fix it: x <- 5 if (x > 0) { print("positive") } else { print("non-positive") }
PROBLEM 2BASIC CALCULATION
Write an R function absolute_value(x) that uses a single if/else statement (not abs()) to return the absolute value of a scalar numeric input x. What does your function return for absolute_value(-7.3)?
PROBLEM 3INTERMEDIATE
Write an R function fizzbuzz(n) that takes a single positive integer n and returns "FizzBuzz" if divisible by both 3 and 5, "Fizz" if divisible by 3 only, "Buzz" if divisible by 5 only, and the number itself otherwise. Pay attention to the order of conditions.
PROBLEM 4APPLIED
You are writing a data-cleaning function clean_temperature(temp, unit) that converts a temperature to Celsius. The unit argument is a character string: "C", "F", or "K". Implement the function using an else-if chain, include a guard clause that stops with an informative error for unsupported units, and test it with clean_temperature(212, "F").
PROBLEM 5CRITICAL THINKING
Consider this code snippet: vals <- c(10, -3, 0, 7, -1) result <- if (vals > 0) "positive" else "non-positive" (a) What will R do when this code runs, and why? (b) Rewrite the code to correctly produce a character vector classifying each element of vals as "positive" or "non-positive". (c) Discuss why R's design distinguishes scalar if/else from vectorized ifelse() rather than making if operate element-wise.

Summary — If/Else & Else-If Chains in R

R's if/else construct evaluates a scalar logical condition and directs execution into exactly one of two code blocks. By chaining else if clauses, you build a sequential decision pipeline that tests conditions top-to-bottom and short-circuits on the first TRUE. Because the entire construct is an expression in R, you can assign its return value directly to a variable. Remember that else must share a line with the preceding closing brace to avoid parser errors.

For vectorized conditional logic over entire columns or vectors, use ifelse() for two-way branches or dplyr::case_when() for multi-branch classification within data-wrangling pipelines. Use guard clauses at the top of functions to validate inputs early, and favor flat else-if chains over deeply nested conditionals for maintainability. These patterns form the foundation for robust function design, switch()-based dispatch, and eventually S3/S4 method dispatch in advanced R programming.

Varsity Tutors • R Programming • If/Else & Else-If Chains