Debugging R

Hanne Oberman

Junior assistant professor at Utrecht University

28 Sep 2026

Attribution and reuse

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA4.0).

These materials have been developed in part by Hanne Oberman, Shannon Pileggi, Jon Harmon, and Danielle Navarro.

When using these materials, you must ensure that the content is not used for generative AI model training.

Computer bugs

Bugs in R

Explanation: df is a closure, in this case, the function stats::df(), the density of \(F\)-distribution.

(It is not a data frame, as you thought it was, ha!)

It does not have elements that you could subset.

Learning objectives:

  • Overall: Find and fix errors.
  • Locate exactly where an error occurred with the traceback() function.
  • Pause the execution of a function and launch an environment to interactively explore what’s happening.
  • Debug non-interactively executed code.
  • Recognize other (non-error) problems that occasionally also need debugging.

Preventing errors

The best way to deal with errors is not to have any at all

Use an IDE

Popular IDEs for R:

  • RStudio: Ideal for R programming with features tailored for data analysis and visualization
  • Positron/Visual Studio Code (VS Code): A versatile code editor with a vast ecosystem of extensions for various languages

\(\rightarrow\) visual feedback (e.g., rainbow parentheses)

\(\rightarrow\) use [tab] for code completion

seq(1, 10)

Code style and naming

  • Keep your code clear and legible with a code style
    \(\rightarrow\) Auto-format using [ctrl]/[cmd] + [shift] + A
  • Adhere to naming conventions/rules
    • Object names in R cannot start with a digit and cannot contain certain other characters such as a comma or a space
    • Adopt a convention for demarcating words in names
i_use_snake_case
other.people.use.periods
evenOthersUseCamelCase

\(\rightarrow\) [tab] for code completion

this_is_a_really_long_name <- 2.5

Always start with a blank slate

  • Code in a fresh R session
  • Load only the necessary packages and objects
  • Save objects for code that takes a long time to execute
saveRDS(my_precious, here("results", "my_precious.rds"))
  • Access previously saved objects
my_precious <- readRDS(here("results", "my_precious.rds"))

File naming

Some file names are good ✅

  • reading01_shakespeare_romeo-and-juliet_act01.docx
  • reading01_shakespeare_romeo-and-juliet_act02.docx
  • reading02_shakespeare_othello.docx
  • reading19_plath_the-bell-jar.docx

Other file names are bad ❌

  • Romeo and Juliet Act 1.docx
  • Shakespeare RJ act2.docx
  • shakespeare othello I think?.docx
  • belljar plath (1).docx

Three principles for file naming

  • Be nice to machines
  • Be nice to humans
  • Make sorting and searching easy

Names machines like

  • Don’t leave white space
    • Machines sometimes get confused by spaces
    • ✅ romeo-and-juliet_act01.docx
      ❌ romeo and juliet act 1.docx
  • Only use letters, numbers, hyphens and underscores
    • Don’t assume case is meaningful (some operating systems treat a and A the same)
    • Machines sometimes get confused by special characters
    • If you can, avoid accents ñ (but consider accessibility!)
    • ✅ essay_romeo-and-juliet_draft01.docx
      ❌ essay “romeo and juliet” draft01(1).docx

Names machines like

  • Use separator characters
    • Use hyphen - to mean “different words that are part of the same chunk”
    • Use underscore _ to separate different chunks
    • Chunks: [identifier] [author] [title] [section (optional)]
    • ✅ reading01_shakespeare_romeo-and-juliet_act01.docx
    • ✅ reading01_shakespeare_romeo-and-juliet_act02.docx
    • ✅ reading02_shakespeare_othello.docx
    • ✅ reading19_plath_the-bell-jar.docx

Names humans like

  • Love the slug 🐌

    • “analysis01_descriptive-statistics.R”
    • “analysis02_preregistered-analysis.R”
    • “notes01_realising-the-problem.txt”
    • “analysis03_departing-from-the-plan.R”
    • “notes02_tentative-write-up.docx”
  • Concise, meaningful description

  • Usually appended to the end

Easy to sort and search names

  • File names sometimes need to include dates

  • … but these dates don’t sort in chronological order

    • 1-April-2012.R
    • 1-Jan-2009.R
    • 12-Jan-2012.R
    • 2-Jan-2012.R
  • Happily, these dates do!

    • 2009-01-01.R
    • 2012-01-01.R
    • 2012-01-02.R
    • 2012-04-01.R

Easy to sort and search names

Easy to sort and search names

  • Use numbers as a prefix to order files

    • 01_preface.docx
    • 02_introduction.docx
    • 03_method.docx
    • etc…
    • 19_appendix-09.docx
    • 20_appendix-10.docx
    • 21_appendix-11.docx
  • Left pad with 0 so that all numbers have same length

  • Numbers are more robust than letters (alphabetical order varies)

Three principles

  • Be nice to machines
  • Be nice to humans
  • Make sorting and searching easy

Use an explicit project structure

setwd("path/that/only/works/on/my/machine")
  • Use R project (.Rproj) files to organize your files
    • No more need for setwd()
    • All relative paths from .Rproj file
  • Use here::here()
    • Create paths relative to the R script
    • Grab files from folders inside top-level directory here("data", "file_i_want.csv")

File paths

Three files inside one folder

  • 📂 project
    • 📄 01_initial-plan.docx
    • 📄 02_data-collection.R
    • 📄 03_initial-analysis.R

The “file paths” are written like this

project/01_initial-plan.docx
project/02_data-collection.R
project/03_initial-analysis.R

File paths

Three files inside one folder

  • 📂 project
    • 📄 01_initial-plan.docx
    • 📄 02_data-collection.R
    • 📄 03_initial-analysis.R

Except on Windows…

project\01_initial-plan.docx
project\02_data-collection.R
project\03_initial-analysis.R

File tree

  • 📂 project
    • 📄 overview.docx
  • 📂 data
    • 📄 data_ex01.csv
    • 📄 data_ex02.csv
  • 📂 analysis
    • 📄 ex01_01_descriptive-statistics.R
    • 📄 ex01_02_inferential-statistics.R
    • 📄 ex02_01_descriptive-statistics.R
    • 📄 ex02_02_inferential-statistics.R
  • 📂 report
    • 📄 2021-03-11_report_getting-started.docx

File tree

  • project/overview.docx
  • project/data/data_ex01.csv
  • project/data/data_ex02.csv
  • project/analysis/ex01_01_descriptive-statistics.R
  • project/analysis/ex01_02_inferential-statistics.R
  • project/analysis/ex02_01_descriptive-statistics.R
  • project/analysis/ex02_02_inferential-statistics.R
  • project/report/2021-03-11_report_getting-started.docx

But what if

Strategies for finding and fixing errors

Debugging is like being the detective in a crime movie where you’re also the murderer.

-Filipe Fortes

Types of errors

  • Parsing errors: you are missing a closing ), } or " so the code AST cannot even be formed
    • Usually captured by IDEs such as RStudio and Positron with red flashing pointers
  • Syntax errors:
    • Parentheses are matched but one of them closes at wrong time
    • The name of a function input is misspelled
    • Passing more inputs to a function than it expects
  • Runtime errors: the code is syntactically correct, but it does a weird thing in your actual environment with your actual data

Common syntax errors

Longer list here

  • Parenthesis mismatches

  • [[...]] vs. [...]

  • == vs. =

  • Comparing real numbers exactly using == after calculations that result in non-integers

  • Type coercion and dropping dimensions

inherits(mtcars[,1], "data.frame")
[1] FALSE
inherits(mtcars[,1, drop=FALSE], "data.frame")
[1] TRUE

More common syntax errors

  • You expect a single value but your code gives you a vector

\(\rightarrow\) you may need identical() or all(), or form the condition more thoroughly

Troubleshooting

  1. SEARCH
    Teach Me How To Google, Samantha Csik (2022)
    slides, recording

  2. RESET
    Restart R, especially when things get weird.

  3. REPREX
    make a reprex….. please, Sharla Gelfand (2021)
    slides, recording, https://reprex.tidyverse.org/

  4. AI (optional)
    Claude, ChatGPT, etc.

1. Search first!

2. Resetting R

Have you tried turning it OFF and ON again?

Session -> Restart R or

  • Ctrl + Shift + F10 (Windows),

  • Cmd + Shift + 0 / Cmd + Shift + F10 (Mac)

usethis::use_blank_slate()

or Tools -> Global Options -> Workspace

Why reset?

Which persist after rm(list = ls())?

Option Persists?
A. library(dplyr)
B. summary <- head
C. options(stringsAsFactors = FALSE)
D. Sys.setenv(LANGUAGE = "fr")
E. x <- 1:5
F. attach(iris)

Why reset?

Which persist after rm(list = ls())?

Option Persists?
A. library(dplyr)
B. summary <- head
C. options(stringsAsFactors = FALSE)
D. Sys.setenv(LANGUAGE = "fr")
E. x <- 1:5
F. attach(iris)

rm(list = ls())

  • ‘object-nuking’ command: delete user-created objects from the global workspace
  • makes your script vulnerable to hidden dependencies
    • loaded packages
    • settings such as stringsAsFactors
    • relative paths
  • deceptive ‘reproducibility’

Instead, save objects, reset and/or use sessioncheck()

sessioncheck::sessioncheck()
Warning: Session check results:
✖ Unexpected objects in global environment: fa_fill
✖ Unexpected packages: countdown
✔ No unexpected environments attached

Your turn, exercise 00

Set your Workspace options as shown.

usethis::use_blank_slate() or

Tools -> Global Options -> Workspace

3. Repeatable bugs are more debuggable

  • May need to execute many times
  • Up-front investment to make a {reprex} pays off
  • A reproducible example is a minimal and self-contained code snippet that shows exactly what happened when the code was run
    • Is the example minimal? Remove unrelated code and data, etc.
    • Is it self‑contained? All libraries loaded, seeds set, data available, etc.

The reprex() function

  • reprex() records:
    • the R expressions you pass (or the selected code in RStudio)
    • the console output, messages, warnings, and plots
  • The minimal, self‑contained snippet speeds up diagnosing problems
  • The output can be copied directly into forums, issue trackers, research and teaching materials
  • It enforces the reproducibility principle: anyone who receives the snippet can run it on their own machine and obtain the exact same result

A reprex

A <- 1:3
mean(A)
[1] 2
mean(a)
Error:
! object 'a' not found


reprex::reprex()


A <- 1:3
mean(A)
#> [1] 2
mean(a)
#> Error:
#> ! object 'a' not found

Created on 2026-09-24 with reprex v2.1.1

Use the scientific method to identify bugs

  • Generate a hypothesis.
  • Design experiments to test that hypothesis.
  • Record your results.

Being systematic often saves time in the end.

Debugging in practice

Finding your bug is a process of confirming the many things that you believe are true — until you find one which is not true.

—Norm Matloff (emphasis added)

Checklist


R installed? Pretty recent?

\(\rightarrow\) Recommended R ≥ 4.4.0

RStudio installed?

\(\rightarrow\) I’m on 2026.08.1+195


Optional:

Ready to build packages?

\(\rightarrow\) devtools::has_devel()
\(\rightarrow\) Your system is ready to build packages!

Sourcing


⚠️ name your script with functions

⚠️ source your script with functions

for the best debugging experience 😎


Key concepts


traceback

\(\rightarrow\) location where did the error occur


interactive debugger

\(\rightarrow\) context why did the error occur


your code vs their code

Debugging tools

This is a lot.


These tools achieve similar objectives slightly differently.


People don’t generally use all of these tools at once. They pick and choose the ones they like.

Debugging your code

Debugging your code

Set up


f <- function(x) {
  x + 1
}

g <- function(x) f(x)

g("a")
Error in `x + 1`:
! non-numeric argument to binary operator

traceback

Use traceback() to locate the error


source("demo/my_functions.R")
g("a")
traceback()


shows the sequence of calls that lead to the error.

Richer traceback


source("demo/my_functions.R")
options(error = rlang::entrace) 
g("a")
rlang::last_trace()


options(error = rlang::entrace)
could go in your .Rprofile

Example: Chained functions

traceback() shows the call stack that lead to the error

Click “Show traceback”::

Read bottom to top.

traceback() limits

In real world, traceback may look like 25 layers

  • Your code is the first 2-3 layers.
  • The last 5 layers are very clearly formatting the error and making a safe return of appropriate objects.
  • The middle 15 layers are classes and methods of dplyr, rlang, purrr and base and maybe packages you have never heard of, passing very opaque things to one another, your informatively named objects are abstracted somewhere in ..., .x and envir…
  • … and you are still left wondering what’s going on.

interactive debugger

f <- function(x) {
  browser()
  x + 1
}

g <- function(x) f(x)


browser() opens the interactive debugger.

  1. Modify the function by inserting a browser() statement.

  2. Source the function.

  3. Execute the function.

Interactive debugger

Use browser() to set a break point in code

browser() provides special commands

These commands work in the Console; RStudio also makes the toolbar buttons available.

  • Next (n): Execute the next step
  • Step into (s): Dive into function (or n)
  • Finish (f): Finish execution of the current loop/function
  • Continue (c): Continue regular execution of the function (leave interactive)
  • Stop (Q): Stop debugging, terminate the function, and return to the global workspace

Interactive debugger overview

Investigate objects

ls(), ls.str(),
str(), print()

Control execution

command operation
n next statement
c continue (leave interactive debugging)
s step into function call
f finish loop / function
where show previous calls
Q quit debugger

browser() practical tips

  • RStudio runs its best attempt to show the code that is being executed in the “Source” pane
    • parsed AST for external functions (no comments, no indents);
    • actual source files within your packages when you are in the package project.
  • That means you can select the code and Ctrl+Enter to execute it
    • if in doubt about the source of the error, do that rather than hitting n “Next”
    • especially if the computations leading to this point in your code are costly in terms of time

Debugging your code

Your turn, exercise 01

Download the exercises

  • download the zip folder from Brightspace

Complete 01_exercise to practice debugging your own code.

  • 01_debugging_spartan.R (directions to explore without suggested code)

  • 01_debugging_comfy.R (directions to explore with suggested code)

  • 01_debugging_solution.R (directions to explore with code solutions)

Debugging in the IDE

Debugging in RStudio

RStudio has tools for debugging

  • Click “Rerun with Debug” in error message
  • Enable Debug > On Error > Break in Code to always jump to error
  • Set breakpoints in RStudio for virtual browser()

Editor breakpoints

red circle = breakpoint = browser()

(but you don’t have to change your code)

Set / re-set an editor breakpoint:

  • click to the left of the line number in the source file

  • press Shift+F9 with your cursor on the line

Editor breakpoints

red circle = breakpoint = browser()

(but you don’t have to change your code)

To activate, either

  • click IDE Source button, or

  • debugSource(“demo/my_functions.R”)

Debugging console

IDE on error

Automatically invoke actions on error.

New set up for demonstration.

f <- function(x) { strsplit(x, ",") }
g <- function(x) { f(x) }
g("a,b")
[[1]]
[1] "a" "b"


g(factor("a,b"))
Error in `strsplit()`:
! non-character argument

IDE message only


IDE error inspector


IDE break in code


Error inspector

# IDE Error Inspector not triggered
f <- function(x) x + 1
g <- function(x) f(x)
g("a")

# IDE Error Inspector not triggered
strsplit(factor("a,b"), ",")

# IDE Error Inspector not triggered
f <- function(x) strsplit(x, ",")
f(factor("a,b"))

# IDE Error Inspector yes triggered
g <- function(x) f(x)
g(factor("a,b"))


The error inspector is only invoked if your code is involved.

Debugging in the RStudio IDE

Your turn, exercise 02


Complete 02_exercise to practice debugging a slightly different version of your own code, using features from the RStudio IDE.

Debugging their code

Debugging their code

debug()

debug() = browser()/ breakpoint in first line of function

# source if code is yours, otherwise not necessary
source("demo/my_functions.R")
# set debugging flag
debug("g")
g("a")
# turn off debugging flag
undebug("g")
  • interactive debugger is initiated every time g() is executed, until undebug("g")

  • depending on function internals, this can… trap you in the debugger 😬

debugonce()

debugonce() = browser()/ breakpoint in first line of function

# source if code is yours, otherwise not necessary
source("demo/my_functions.R")
# set debugging flag
debugonce("g")
g("a")
  • one time only!

  • interactive debugger initiated a single time when g() is executed

debug(fn_name) to insert browser() in first line of fn_name()

  • undebug(fn_name) to remove it
  • debugonce(fn_name) to do it once (similar to rerun with debug)
  • utils::setBreakpoint("file_name", line_number)

options(error = recover)

we already discussed
options(error = rlang::entrace)
for a richer traceback on error


🤠 options(error = recover)

  1. displays an interactive prompt with frames

  2. you select the frame to enter the debugger

Use options(error = recover) for interactive debugging prompt

Turn off with options(error = NULL)

recover example

source("demo/my_functions.R")
options(error = recover)
g("a")
options(error = NULL)


trace() overview

trace(what = fun, tracer = browser)
is equivalent to

  • inserting browser() in first line of function

  • debug(fun)

you an also insert any code at any location in function

  • trace(what = fun, tracer = browser, at = 2)
    inserts browser() at second step of fun

untrace(fun) cancels the tracing

trace() without a specified step


trace(what = colSums, tracer = browser)
colSums(1:3)
# ls.str() in browser
untrace(colSums)


trace(what = colSums, tracer = browser)
is equivalent to

  • inserting browser() in first line of colSums
    if we had the source code

  • debug(colSums)

identify function step

trace() at specified step


# identify spot to insert code
as.list(x[[3]][[1]])
# equivalent notation
as.list(x[[c(3, 1)]])

# insert browser at step 3.1
trace(colSums, browser, at = list(c(3, 1)))
# execute function
colSums(1:3)
# ls.str() handy in browser
# cancel tracing
untrace(colSums)

Debugging their code

Your turn, exercise 03


# install.packages("pak")
# pak::pak("rstats-wtf/wtfdbg")


Complete 03_exercise to practice debugging others’ code.

Special cases

Quarto/Rmarkdown


Troubleshooting:

  1. rmarkdown chunk option error = TRUE enables knitting with errors

  2. insert knitr::knit_exit() and interactively work through .Rmd


Debugging:

  1. Adv R Ch 22.5.3 RMarkdown https://adv-r.hadley.nz/debugging.html#rmarkdown

  2. WTF Ch 11.4 Debugging in Rmarkdown documents https://rstats.wtf/debugging-r-code.html#debugging-in-r-markdown-documents

Piped expressions


tracebacks can be verbose with pipes


options(error = rlang::entrace, rlang_backtrace_on_error = "branch")

gives trimmed tracebacks when using pipes

Warnings


If you want to dig deeper into a warning, you can convert them to errors to initiate debugging tools.

?options
options(warn = 0) # default, stores warnings until top-level function returns
options(warn = 1) # warnings are printed as they occur
options(warn = 2) # upgrades warnings to errors


# initiate recover on warning
options(warn = 2, error = recover)
# restore original settings
options(warn = 0, error = NULL)

Debugging special cases

Wrap up

Pick your favorite

Watch out!

Functions can fail without errors

  • Unexpected warning: options(warn = 2) turns warnings into errors.
  • Unexpected message: Proposed solution in book no longer available.
  • Function might never return. Terminate & traceback().
  • Crashed R = bug in compiled (C, C++, etc) code.

traceback vs rlang functions


  • RStudio displays calls in the same order as traceback()
  • {rlang} functions use the same ordering & numbering as recover()

Go forth,
and learn from your bugs!

traceback


interactive debugger


your code   vs  
their code

Further reading

Some useful resources on debugging:

Additional resources

Advanced topics

Use callr::r() or fresh start to look for differences

callr::r(f, list(1, 2)) calls f(1, 2) in a fresh session

  • Global env
  • Packages
  • Object
  • Working directory
  • PATH environment variable
  • R_LIBS environment variable

dump.frames() is the equivalent to recover() for non-interactive code.

Advanced: traceback() is confusing with lazy evaluation

Advanced: browser() can be conditional

A better practice would be to define your own debugging flags that would be turned off or disappear in production code

if (exists("my_debugging_flag")) browser()

Advanced: RStudio vs Positron

Debugging tools in Positron are still under active development. browser() and debugonce() are supported, but breakpoints aren’t.

Advanced: Automated tests help

  • Add tests for nearby working code to avoid new bugs.
  • Add tests for specific broken cases.
  • Add tests for hypotheses.

Concepts of unit tests are generally associated with R package development, see chapters on testing in “Writing R Packages”.

Automated testing sometimes brings a dilemma: you add something to a working function, and the stuff that you added does work according to the new tests, but an old test gets broken. This is may not be an error per se, but it shows a gap in your knowledge of and assumptions about the code (see Norm Matloff’s quote above) and you have to deal with that one way or another.

Advanced: Debugging withing packages

See Advanced R book (chapter 8) on classed conditions:

  • you can create richer structured objects with
rlang::abort(
  message = "cli-formatted-message",
  class   = "your-special-class-to-distinguish-from-other-errors",
  ...
)
  • you can pass your situation report in ... so that error handlers and/or reporters can more effectively peek into your environment that errored
  • you can capture the class with testthat::expect_s3_class()

Advanced: Automate code style checking

lintr can automate checks to warn you if you leave browser() statements in your code.


https://lintr.r-lib.org/reference/default_undesirable_functions.html


usethis::use_github_action("lint")

Live example in devtools https://github.com/r-lib/devtools/actions/workflows/lint.yaml

Advanced: Debugging Rmarkdown/Quarto has challenges

  • Call rmarkdown::render("path/to/file.Rmd") instead of IDE knitting.
    • downside: the content of your current environment propagates into the markdown code
  • Use sink() for tricksy error handling

See also “Markdown test drive” in Jenny Bryan’s Happy Git With R book.

References