10  Debuggers

Published

2025-01-20

Warning

The contents for this section are under development. Thank you for your patience.

Bugs can cause our app to crash, produce incorrect results or displays, and result in other unexpected behaviors. Fixing bugs in our app-package is an important step to ensure our application continues behaving as expected. This chapter will cover how to use an interactive debugger to help us find the root cause of bugs and errors.

TLDR: Interactive Debugger & Shiny Apps

Placing a call to browser() inside observe() will trigger the interactive debugger when the observer is invalidated.

observe({
  browser()
})

This allows us to interactively examine variables and reactive expressions (within the scope of the observe() function).

Debugging in RStudio is covered elsewhere,1 so we’ll focus on debugging our Shiny app code using Positron’s interactive debugger.

I’ve created the shinypak R package in an effort to make each section accessible and easy to follow. Install shinypak using pak (or remotes):

install.packages('pak')
pak::pak('mjfrigaard/shinypak')
library(shinypak)

List the apps in this chapter:

list_apps(regex = '10')

Launch apps with launch()

launch(app = '10_debugger')

Download apps with get_app()

get_app(app = '10_debugger')
Positron Version

At the time of this writing, the 2025.01.0-39 pre-release of Positron was available for testing.

10.1 Interactive debugging

Interactive debugging (e.g., using browser() or setting a breakpoint) allows us to ‘peek inside’ a function’s scope to view intermediate values/variables and break down the execution line by line.

%%{init: {'theme': 'neutral', 'themeVariables': { 'fontFamily': 'monospace', "fontSize":"16px"}}}%%
flowchart LR
        Start(["Begin<br>Execution"])
        Browser{{"Breakpoint<br>or <code>browser</code>?"}}
        subgraph Debugger ["<strong>Debugger</strong>"]
            Step("Enter<br>debugger")
            Step --> |"Step Through<br>Code"|Debug{{"Done<br>debugging?"}}

            Debug -->|"No"| Step
            Debug -->|"Yes"| Exit("Exit<br>debugger")
    
        end
        Resume(["Resume<br>Execution"])
    
    Start --> Browser
    Browser -->|"Yes"| Debugger
    Exit --> Resume
    Browser -->|"No"| Resume
    
    %% style FunctionScope fill:#FFF,color:#000,stroke:#333,font-size:14px
    %% style Start fill:#FFF,stroke:#333,stroke-width:1px,color:#000,font-size:14px
    %% style Browser fill:#ffcccb,stroke:#e60000,stroke-width:3px,color:#000,font-size:14px
    %% style Debugger fill:#F8F8FA,stroke:#4CB7DB,stroke-width:3px,rx:10,ry:10,font-size:14px
    %% style Step1 fill:#4CB7DB,color:#000,stroke:none,rx:15,ry:15,font-size:14px
    %% style Step2 fill:#4CB7DB,color:#000,stroke:none,rx:15,ry:15,font-size:14px
    %% style Debug fill:#ffcccb,stroke:#e60000,stroke-width:3px,color:#000,font-size:14px
    %% style Exit fill:#4CB7DB,color:#000,stroke:none,rx:15,ry:15,font-size:14px
    %% style Resume fill:#FFF,stroke:#333,stroke-width:1px,color:#000,font-size:14px

Interactive debugger with browser() or breakpoint

Unfortunately, using browser() and breakpoints are not as straightforward within reactive contexts. browser() interacts with the R interpreter by temporarily suspending execution and redirecting the input to evaluate R expressions in the context of the function or script that called it.

%%{init: {'theme': 'neutral', 'themeVariables': { 'fontFamily': 'monospace', "fontSize":"14px"}}}%%
flowchart TD
        Running(["Being<br>execution"]) 
            subgraph Browse["<strong>browser() Scope</strong>"]
                Browser("<code>browser()</code>")
                subgraph Debugger["<strong>Interactive Debugger</strong>"]
                 Step[\"Inspect variables<br>and values"/]
                end
        end
        Resume(["Resume<br>execution"])

Running -->|"Encounter"| Browser --> |"Enter"|Step -->|"Exit"|Resume
    
    %% subgraphs
    %% style Function fill:#FFF,color:#000,stroke:#333,font-size:14px %%
    %% style BrowserStep stroke:#4CBB9D,color:#000,stroke-width:3px,rx:10,ry:10,font-size:14px  %%
    %% style Debugger fill:#F8F8FA,color:#000,stroke:#4CB7DB,stroke-width:3px,rx:10,ry:10,font-size:14px %%
    
    %% nodes 
    %% style Running fill:#FFF,color:#000,stroke:#333,rx:10,ry:10,font-size:14px %%
    %% style Encounter fill:#4CBB9D,color:#FFFFFF,stroke:none,rx:10,ry:10,font-size:14px %%
    %% style Step1 fill:#4CB7DB,color:#000,stroke:none,rx:15,ry:15,font-size:14px %%
    %% style Step2 fill:#4CB7DB,color:#000,stroke:none,rx:10,ry:10,font-size:14px %%
    %% style ExitBrowser fill:#4CBB9D,color:#FFFFFF,stroke:none,rx:20,ry:10,font-size:14px %%
    %% style Resume fill:#FFF,color:#000,stroke:#333,rx:10,ry:10,font-size:14px %%
    

How browser() works in regular R function


Shiny’s asynchronous execution makes it difficult for browser() to pause the flow of execution, making maintaining an interactive debugging session challenging.

10.1.1 Reactive browsing

In a Shiny context, code inside the server function is executed asynchronously in response to user inputs or reactive dependencies. Shiny does not directly expose the control flow to the user, but we want to pause this execution without altering or stopping the reactive flow.

Fortunately, Shiny already has a function that performs this: observe(). Within a Shiny server() function, any call to observe() creates a reactive observer that ‘listens’ for changes to reactive dependencies (and runs any enclosed code whenever those dependencies change).

%%{init: {'theme': 'neutral', 'themeVariables': { 'fontFamily': 'monospace', "fontSize":"14px"}}}%%
flowchart TD
        Running(["Being<br>execution"]) 
        subgraph Observe["<strong>observe() Scope</strong>"]
                Observer("<code>observe({<br><br>})</code>")
            subgraph Browse["<strong>browser() Scope</strong>"]
                Browser("<code>browser()</code>")
                subgraph Debugger["<strong>Interactive Debugger</strong>"]
                 Step[\"Inspect variables<br>and reactives"/]
                end
            end
        end
        Resume(["Resume<br>execution"])

Running -->|"Encounter"| Observer --> Browser --> |"Enter"|Step -->|"Exit"|Resume

    %% style Server fill:#FFF,color:#000,stroke:#333,font-size:14px
    %% style StartReact fill:#FFF,color:#000,stroke:#333,font-size:14px
    %% style EndReact fill:#FFF,color:#000,stroke:#333,font-size:14px

    %% style Observer fill:#FFF,stroke:#FEDBC1,stroke-width:3px,font-size:14px
    %% style ObserveExecution fill:#FEDBC1,stroke:none,rx:10,ry:10,font-size:14px
    
    %% style Browser fill:#4CBB9D,color:#000,stroke:none,rx:20,ry:10,font-size:14px
    %% style Browse stroke:#4CBB9D,color:#000,fill:#FFF,stroke-width:3px,rx:10,ry:10,font-size:14px
    
    %% style Debugger fill:#F8F8FA,stroke:#4CB7DB,stroke-width:3px,rx:10,ry:10,font-size:14px
    %% style Step fill:#4CB7DB,stroke:none,rx:10,ry:10

    %% style ExitBrowser fill:#4CBB9D,color:#000,stroke:none,rx:20,ry:10,font-size:14px
    %% style ExitObserver fill:#FEDBC1,color:#000,stroke:none,rx:20,ry:10,font-size:14px

Reactive browser() in an observe()er

When browser() is called from within the observe() scope, the code execution pauses and temporarily suspends the reactive flow so we can inspect the environment (without altering or stopping the reactive flow).

10.2 Example: ggplot2movies app

In the Resources chapter we developed a slight variation of our app with the ggplot2movies data.2 In the following sections we’re going to use the interactive debugger to see the inner workings of the ‘Remove missing’ checkbox.

Remove missing checkbox in ggplot2movies development application (click to enlarge)

Remove missing checkbox in ggplot2movies development application (click to enlarge)

We’ll begin by placing the observe() and browser() breakpoint inside the module server function containing the ‘Remove missing’ input (right after the moduleServer() function). We’ll close the observe() scope after the reactive inputs() are created:

show/hide debugging functions in the module server function
dev_mod_scatter_server <- function(id, var_inputs) {
  moduleServer(id, function(input, output, session) {
    observe({
      browser()
      
      all_data <- fst::read_fst("tidy_movies.fst")

      graph_data <- reactive({
        if (input$missing) {
          tidyr::drop_na(data = all_data)
        } else {
          all_data
        }
      }) |>
        bindEvent(input$missing)

      inputs <- reactive({
        plot_title <- tools::toTitleCase(var_inputs()$plot_title)
        list(
          x = var_inputs()$x,
          y = var_inputs()$y,
          z = var_inputs()$z,
          alpha = var_inputs()$alpha,
          size = var_inputs()$size,
          plot_title = plot_title
        )
      })
    })
    
    observe({
      output$scatterplot <- renderPlot({
        plot <- sap::scatter_plot(
          df = graph_data(),
          x_var = inputs()$x,
          y_var = inputs()$y,
          col_var = inputs()$z,
          alpha_var = inputs()$alpha,
          size_var = inputs()$size
        )
        plot +
          ggplot2::labs(
            title = inputs()$plot_title,
            x = stringr::str_replace_all(
              tools::toTitleCase(inputs()$x), "_", " "),
            y = stringr::str_replace_all(
              tools::toTitleCase(inputs()$y), "_", " ")
          ) +
          ggplot2::theme_minimal() +
          ggplot2::theme(legend.position = "bottom")
      })
    }) |>
      bindEvent(graph_data(), inputs())
    
  })
}
1
Observer scope
2
Call to browser() (execution paused)
3
Read tidy_movies.fst data
4
Missing data checkbox logic
5
Reactive values from user inputs
6
Module graph code (outside of observe() scope)

Then we’ll load the changes:


Ctrl/Cmd + Shift + L

And launch the app using:

launch_app(app = "ggp2")

Launch app with the shinypak package:

launch('10_debugger')


Don’t forget to load any debugging calls with devtools::load_all() before re-launching the app!

devtools::load_all('.')

Or

Ctrl/Cmd + Shift + L

As noted above, browser() pauses code execution and activates the interactive debugger mode, allowing us to view objects, execute code, and ‘step through’ the function line-by-line.

10.2.1 IDE Changes

When the app is launched, Positron alerts us that we’re in debugging mode by making a few changes to the IDE:3

Editor

The dev_mod_scatter.R file in the Editor highlights the browser() function in yellow

Debugger in file Editor (click to enlarge)

Debugger in file Editor (click to enlarge)

Console

The Console tells us browser() was ‘Called from: observe()’ and displays the interactive debugger prompt:

Debugger in file Editor (click to enlarge)

Debugger in file Editor (click to enlarge)

observe() does not inherently pause or interrupt other reactive processes—it just triggers when changes occur within its scope. So when we’re using observe() for debugging, we need to define the context (or scope) for its behavior.

The output in the Console tells us the tidy_movies.fst data are downloaded, but our placement suspends the execution of the application before these data are loaded and the graph is rendered in the UI.

Debugger in Console (click to enlarge)

Debugger in Console (click to enlarge)

Suspended ggplot2movies data app (click to enlarge)

Suspended ggplot2movies data app (click to enlarge)

The interactive debugger can only access variables and values inside the observe() scope, but this process can be incredibly useful for addressing bugs (and for exploring how an application works). In the next sections, we’ll ‘step through’ the module function to explore how the missing values are removed from the graph.

10.2.2 Variables and values

We want to use the interactive debugger to proceed through the module function until the data object enters the logic for the missing checkbox, and then we can confirm its structure.

Step through/execute each line of code by entering n in the Console.

Browse[1]> n

As we ‘step through’ the function, Positron’s Console displays the debug at location, followed by the code line number:

Full path and line number to the file containing our call to browser() (click to enlarge)

Full path and line number to the file containing our call to browser() (click to enlarge)

In the Editor, the same line of code is highlighted in yellow:

Corresponding line number in Editor (click to enlarge)

Corresponding line number in Editor (click to enlarge)

The line creating graph_data gives us a hint for how the missing data are removed (i.e., with bindEvent()), but we’ll explore this more in Print debugging.

graph_data() reactive creation (click to enlarge)

graph_data() reactive creation (click to enlarge)

Under Locals in the DEBUG VARIABLES sidebar, we can see all_data is listed as a <data.frame>, and graph_data are listed as a <reactive.event>:

Click to enlarge DEBUG VARIABLES in sidebar

Click to enlarge DEBUG VARIABLES in sidebar

In the next section, we’ll explore these variables (and the reactive inputs).

10.2.3 Inspecting variables

We can use the Console to evaluate code while the interactive debugger is running. This comes in handy if we want to check the structure of an object inside a module (like all_data).

Browse[1]> str(all_data)
'data.frame':   58788 obs. of  10 variables:
$ title      : chr  "$" "$1000 a Touchdown" ...
$ year       : int  1971 1939 1941 1996 1975 ...
$ length     : int  121 71 7 70 71 91 93 25 97 ...
$ budget     : int  NA NA NA NA NA NA NA NA NA ...
$ rating     : num  6.4 6 8.2 8.2 3.4 4.3 5.3 ...
$ votes      : int  348 20 5 6 17 45 200 24 18 51 ...
$ mpaa       : Factor w/ 5 levels "G","PG","PG-13" ...
$ genre_count: int  2 1 2 1 0 1 2 2 1 0 ...
$ genres     : chr  "Comedy, Drama" "Comedy" ...
$ genre      : Factor w/ 8 levels "Action": 6 3 6 ...

This gives us an idea of the total rows before missing are removed.

10.2.4 Inspecting values

The reactive values and inputs can also be viewed in the Console, and we can see graph_data() is ‘bound’ to input$missing with bindEvent(). We can confirm the input$missing value in the Console:

Browse[1]> input$missing
[1] TRUE

This tells us the ‘Remove missing’ checkbox has been selected, and we can verify the missing values have been removed from graph_data():

Browse[1]> identical(
              nrow(tidyr::drop_na(all_data)), 
              nrow(graph_data())
            )
[1] TRUE

Using browser() to ‘step through’ an application gives us a better understanding of the ‘order of execution’, and it lets us see how input$missing and bindEvent() work together to remove the missing values with the checkbox.

Recap

Recap: Interactive Debuggers

During regular development, an interactive debugger can let us inspect variables and execute the code line-by-line. In Shiny functions, the debugger lets us track the execution of reactive expressions and observers, which allows us to unravel reactivity-related issues that are often difficult to diagnose.

browser() and observe() are powerful tools for debugging our application. Navigating a function using the interactive debugger gives us control over the execution of each line.

Please open an issue on GitHub


  1. For an introduction to the IDE’s debugging tools, see Debugging with the RStudio IDE. Debugging is also covered in Advanced R, 2ed and Mastering Shiny.↩︎

  2. You can refresh your memory on the ggplot2movies application in Section 9.2.↩︎

  3. Watch this video to learn more about Positron.↩︎

  4. Previous versions of Positron highlighted the footer in red.↩︎