if (expression) {
# code that runs only when expression is TRUE
...
}15 Control statements
So far your code has run straight down the page: every line executes, once, in order. Real analyses rarely stay that simple. You want R to decide — drop a sample only if its read count falls below a threshold — and to repeat — run the same quality check across every one of a hundred samples without writing the line a hundred times. Control statements are how you express those decisions and repetitions.
This chapter introduces the two workhorses, conditionals (if / else) and loops (for, while, repeat), along with the keywords that steer them (break, next, return) and a first look at error handling with tryCatch(). We close with a note on when not to write a loop at all — because in R, a vectorized function is usually the better tool.
15.1 What you’ll learn
- Branch your code with
if,if/else, and the vectorizedifelse(). - Repeat work with
for,while, andrepeatloops, and choose the right one. - Steer a loop with
break,next, andreturn. - Handle errors and warnings gracefully with
tryCatch(). - Recognize when a vectorized function or an
apply()-family call beats a loop.
Control statements lean heavily on punctuation. Every open parenthesis ( needs a matching close ), and every open brace { needs a matching }. A single missing bracket is the most common reason a control statement refuses to run, so keep them balanced.
Once a control statement opens a { } block, indent everything inside it. The indentation has no effect on R, but it makes it obvious at a glance which lines belong to the block — which matters enormously once you start nesting one statement inside another.
15.2 Conditional statements
15.2.1 if
An if statement evaluates an expression and, only if the result is TRUE, runs the block of code in its braces.
Syntax:
The expression in parentheses must boil down to a single logical value — TRUE or FALSE — and that value decides whether the braced code runs.
Example:
When the condition is TRUE, all the lines in the braces execute — including the assignment that reset x to 0. That’s why printing x afterward shows 0, not 12.
15.2.2 if/else
An if/else statement adds a second block that runs when the expression is FALSE. Exactly one of the two blocks always runs.
Syntax:
if (expression) {
# code that runs when expression is TRUE
...
} else {
# code that runs when expression is FALSE
...
}Read it aloud: if the expression is true, run this code; else (otherwise) run that other code.
Example:
0 is not greater than 0
x[1] 2
Because x was 0 going in, the condition x > 0 is FALSE, so the else branch runs and x becomes 2.
15.2.3 ifelse()
ifelse() is a vectorized cousin of if/else. Instead of testing one value, it tests an entire vector at once and returns a vector of results — one “yes” or “no” value per element.
Syntax:
ifelse(test_expression, yes_value, no_value)Example:
num_vec <- -3:3
ifelse(num_vec >= 0, "positive", "negative")[1] "negative" "negative" "negative" "positive" "positive" "positive" "positive"
15.3 Loops
Loops repeat a block of code: a fixed number of times, once for each element of a collection, or until some condition is met.
15.3.1 for
A for loop runs its body once for each element of a vector or list, assigning the current element to a variable you can use inside the loop.
Syntax:
for (value in vector) {
# code that runs once per element; `value` holds the current one
...
}Examples:
Here we loop over a vector of names, printing how many characters each one has:
[1] "Donna has 5 letters"
[1] "John has 4 letters"
[1] "Bradley has 7 letters"
[1] "Kara has 4 letters"
Here, for each value 1 through 5, we add it to a running total. Notice how x is updated on every pass:
[1] "add 1 to 0"
[1] "add 2 to 1"
[1] "add 3 to 3"
[1] "add 4 to 6"
[1] "add 5 to 10"
x[1] 15
We can also loop over the elements of a list. seq_along() gives us the positions 1, 2, 3, ..., which we use both to pull each element out and to look up its name:
[1] "List element people contains 4 values"
[1] "List element ages contains 5 values"
[1] "List element animals contains 2 values"
15.3.2 while
A while loop keeps running as long as its condition stays TRUE. It checks the condition before each pass, so you must change something inside the loop that eventually makes the condition FALSE.
Syntax:
while (expression) {
# code that runs while expression is TRUE
# remember to update the variable the expression tests
...
}Example:
Starting at 1, we print the value and add 1 each pass, stopping once the value passes 5:
value <- 1
while (value <= 5) {
print(value)
value <- value + 1
}[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
A while loop only ends when its condition becomes FALSE. If nothing inside the loop ever makes that happen, the loop runs forever — an infinite loop. Always make sure the body updates the variable the condition tests. (If you do get stuck in one, press Esc or the stop button to interrupt R.)
15.3.3 repeat
A repeat loop has no condition of its own — it runs forever until a break statement stops it. That makes break mandatory.
Syntax:
repeat {
# code to evaluate
if (condition) {
break
}
}Example:
Here we repeat ourselves until we’ve done it the set number of times. The body increments i, and the if checks whether we’ve hit the limit:
i <- 0
times <- 3
repeat {
print("I am repeating myself")
i <- i + 1
if (i == times) {
break
}
}[1] "I am repeating myself"
[1] "I am repeating myself"
[1] "I am repeating myself"
15.4 Steering a loop: break, next, and return
These keywords let you change a loop’s flow from the inside. They’re especially handy in longer or nested statements.
15.4.1 break
We’ve already seen break with repeat. It stops the loop immediately and exits the moment it’s reached — no further iterations run.
15.4.2 next
next skips the rest of the current iteration and jumps straight to the next one, without ending the loop. Here we print only the even numbers from 1 to 10, using next to skip the odd ones:
15.4.3 return
return exits the surrounding function immediately and hands back a value. It’s used inside functions rather than bare loops.
Syntax:
return(expression)This function takes an argument x. If x is 0 it returns "zero". Otherwise it adds 4; if the result is 0 or less it returns that value, and if not it returns the value doubled:
15.5 Other useful patterns
15.5.1 Nesting
You can place one control statement inside another — we already did, when the repeat loop held an if block. Any statement can nest, to any depth. A classic example is a pair of for loops walking the rows and columns of a matrix.
Here we build a numeric matrix with 5 rows and 3 columns, filled with 1 through 15 by column, then loop over it row by row and print each cell:
mat <- matrix(1:15, ncol = 3)
mat [,1] [,2] [,3]
[1,] 1 6 11
[2,] 2 7 12
[3,] 3 8 13
[4,] 4 9 14
[5,] 5 10 15
[1] 1
[1] 6
[1] 11
[1] 2
[1] 7
[1] 12
[1] 3
[1] 8
[1] 13
[1] 4
[1] 9
[1] 14
[1] 5
[1] 10
[1] 15
c
It’s tempting to use r and c for “row” and “column”, but c is also R’s function for combining values (c(1, 2, 3)). Naming a variable c shadows that function and invites confusing bugs. Using i and j keeps the built-in c() available and is the conventional choice for loop counters.
15.5.2 try() / tryCatch()
Not strictly a control statement, but it fits the theme: tryCatch() lets you handle code that might throw an error or a warning, instead of letting it halt your whole script. This matters when you’re looping over many files or samples and don’t want one bad case to stop the rest.
Syntax:
tryCatch(
expr,
error = function(e) {
# what to do if expr raises an error
},
warning = function(w) {
# what to do if expr raises a warning
},
finally = {
# code that runs no matter what
}
)You can supply any subset of error, warning, and finally. expr is the code you’re attempting. The error handler runs if expr fails; its argument e is an error object carrying the details. The warning handler works the same way for warnings, via w. The finally block runs regardless of the outcome — which makes it the right place to close a file or database connection that expr may have opened.
15.5.3 Vectorization and the apply() family
Here’s the twist: a lot of the work people reach for loops to do, R can do without a loop — and faster. Many functions are already vectorized, meaning they operate on a whole vector at once. To add 1 to every element of a vector, you don’t loop; you just write x + 1.
x <- 1:5
x + 1 # vectorized: no loop needed[1] 2 3 4 5 6
mean(x) # already summarizes the whole vector[1] 3
When there isn’t a ready-made vectorized function, the apply() family applies a function across a structure for you: apply() over the rows or columns of a matrix, and lapply(), sapply(), mapply(), and tapply() over vectors and lists. Before writing a loop, it’s worth asking whether a vectorized function or an apply() call would be clearer and quicker.
15.6 Exercises
-
Even or odd. Write an
if/elsestatement that prints"even"if a numbernis even and"odd"otherwise. (Hint:n %% 2gives the remainder after dividing by 2.) -
Sum a vector with a loop. Using a
forloop, add up the numbers inc(4, 8, 15, 16, 23, 42). Then check your answer with the built-insum().NoteSolutionnums <- c(4, 8, 15, 16, 23, 42) total <- 0 for (n in nums) { total <- total + n } total[1] 108sum(nums) # the vectorized way — same answer, far less typing[1] 108The loop accumulates the running total one element at a time;
sum()does the same thing in one vectorized call. This is exactly the kind of task where a vectorized function beats a hand-written loop. -
Skip the missing values. Loop over
c(3, NA, 7, NA, 2)and print only the values that are notNA, usingnextto skip the missing ones. (Hint:is.na(x)isTRUEwhenxis missing.)
15.7 Summary
You can now control the flow of your code rather than running straight down the page:
-
Branch with
ifandif/elsefor single values, andifelse()to test a whole vector at once. -
Repeat with
for(once per element),while(until a condition flips), andrepeat(until youbreak). -
Steer loops with
break(stop),next(skip), andreturn(exit a function with a value). -
Guard risky code with
tryCatch()so one error doesn’t sink an entire run. -
Reach for vectorization first. Many tasks that look like they need a loop are better done with a vectorized function or an
apply()-family call.