Skip to content

Control Flow

Control flow lets your scripts make decisions and repeat actions. These are essential tools for processing biological data. You will use conditionals to filter results and loops to iterate over genes, samples, or files.

Conditionals let your code take different actions based on a test. In bioinformatics, you often check whether a p-value is significant or whether a file exists before reading it.

The if/else statement runs one block of code when a condition is TRUE and another when it is FALSE.

pvalue <- 0.003
if (pvalue < 0.05) {
cat("Result is statistically significant\n")
} else {
cat("Result is not significant\n")
}
Result is statistically significant

The condition inside if() must evaluate to a single TRUE or FALSE. R will warn you if you pass a vector with more than one element.

You can chain multiple conditions together. This is useful for classifying genes by their fold change direction.

log2fc <- 2.5
if (log2fc > 1) {
cat("Gene is upregulated\n")
} else if (log2fc < -1) {
cat("Gene is downregulated\n")
} else {
cat("No significant change\n")
}
Gene is upregulated

R evaluates conditions from top to bottom. It runs the first block whose condition is TRUE and skips the rest.

Before reading a data file, check that it actually exists. This prevents your script from crashing with a confusing error.

input_file <- "counts.csv"
if (file.exists(input_file)) {
cat("Reading:", input_file, "\n")
} else {
cat("File not found:", input_file, "\n")
}
File not found: counts.csv

This pattern is especially helpful in pipelines where upstream steps may or may not have produced output files.

The if/else statement works on a single value. When you need to apply a condition across an entire vector, use ifelse(). This is the R way to classify every element at once without writing a loop.

pvalues <- c(0.001, 0.23, 0.04, 0.87, 0.005)
significance <- ifelse(pvalues < 0.05, "significant", "not significant")
significance
[1] "significant" "not significant" "significant" "not significant"
[5] "significant"

You can nest ifelse() calls to create three or more categories. Here we classify genes as upregulated, downregulated, or not significant based on log2 fold change.

log2fc_values <- c(2.5, -0.3, -1.8, 0.1, 3.2)
direction <- ifelse(log2fc_values > 1, "up",
ifelse(log2fc_values < -1, "down", "ns"))
direction
[1] "up" "ns" "down" "ns" "up"

Nested ifelse() can become hard to read beyond two or three levels. For more complex cases, consider dplyr::case_when().

A for loop repeats a block of code once for each element in a vector. This is useful when you need to process a list of genes, samples, or files one at a time.

genes <- c("TP53", "BRCA1", "EGFR", "KRAS")
for (gene in genes) {
cat("Processing gene:", gene, "\n")
}
Processing gene: TP53
Processing gene: BRCA1
Processing gene: EGFR
Processing gene: KRAS

Sometimes you need the position of each element, not just its value. Use seq_along() to generate index numbers safely. It handles empty vectors correctly, unlike 1:length(x).

samples <- c("S1", "S2", "S3")
for (i in seq_along(samples)) {
cat("Sample", i, ":", samples[i], "\n")
}
Sample 1 : S1
Sample 2 : S2
Sample 3 : S3

When you need to store results from each iteration, pre-allocate a vector first. This is much faster than growing a vector inside the loop.

counts <- c(100, 250, 50, 300, 75)
log_counts <- numeric(length(counts))
for (i in seq_along(counts)) {
log_counts[i] <- log2(counts[i] + 1)
}
log_counts
[1] 6.658211 7.971544 5.672425 8.233620 6.247928

R is designed for vectorized operations. They run faster than loops because R processes the entire vector in optimized C code under the hood. Whenever possible, prefer vectorized operations over explicit loops.

The loop above can be replaced with a single line:

log_counts_vec <- log2(counts + 1)
log_counts_vec
[1] 6.658211 7.971544 5.672425 8.233620 6.247928

The results are identical. The vectorized version is shorter, easier to read, and faster on large datasets.

A while loop keeps running as long as its condition is TRUE. This is useful when you do not know in advance how many iterations you need.

Here we simulate how many times a cell population must double to exceed a threshold:

threshold <- 100
current <- 10
doublings <- 0
while (current < threshold) {
current <- current * 2
doublings <- doublings + 1
}
cat("Reached", current, "after", doublings, "doublings\n")
Reached 160 after 4 doublings

Be careful with while loops. If the condition never becomes FALSE, your script will run forever. Always make sure something inside the loop moves toward ending the condition.

Inside a loop, next skips to the next iteration and break exits the loop entirely. These are useful for handling missing data or stopping early when a condition is met.

pvalues <- c(0.001, NA, 0.04, 0.5, 0.003)
for (i in seq_along(pvalues)) {
if (is.na(pvalues[i])) {
cat("Skipping NA at position", i, "\n")
next
}
if (pvalues[i] < 0.01) {
cat("Found highly significant result at position", i, "\n")
}
}
Found highly significant result at position 1
Skipping NA at position 2
Found highly significant result at position 5

Use next to skip problematic entries like NA values. Use break when you only need the first match and want to stop searching.

R was built for working with vectors and matrices. Vectorized functions operate on entire vectors in a single call. They are almost always faster and more concise than writing an explicit loop.

Here are key reasons to prefer vectorized code:

  • Speed. Vectorized functions call optimized C or Fortran code internally. Loops in R are interpreted and carry overhead on each iteration.
  • Readability. A single line like log2(counts + 1) communicates intent more clearly than a five-line loop.
  • Less room for bugs. Fewer lines of code means fewer places for mistakes. You avoid common loop errors like off-by-one indexing or forgetting to pre-allocate.

That said, loops are not always wrong. Some tasks are genuinely sequential. Reading multiple files or calling an API for each sample requires iteration. Use the right tool for the job.

For applying functions across vectors and lists without writing explicit loops, R provides the apply family: apply(), sapply(), lapply(), and others. These are covered in the Functions page.

Construct Purpose Example use case
if/else Branch on a single condition Check if a p-value is significant
if/else if/else Branch on multiple conditions Classify a gene as up, down, or unchanged
ifelse() Vectorized conditional Label an entire column of p-values
for Iterate over a vector Process each sample file
while Loop until a condition is met Simulate growth until a threshold
next Skip current iteration Skip NA values in a loop
break Exit a loop early Stop after finding the first match

You now know how to make decisions and repeat actions in R. The next page covers how to organize your code into reusable functions. Head to Functions to continue.