26  Debugging

Debugging is an essential skill for any programmer, and it’s not restricted to fixing errors or broken code. When used as an exploratory tool, the debugger allows us to ‘look inside’ functions and break down their execution line-by-line. Posit Workbench’s debugging tools are covered elsewhere, so this chapter will focus on debugging Shiny code and functions inside an R package.1

Debugging Shiny Apps

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')

Review the chapters in each section:

library(shinypak)
list_apps(regex = 'debug')
## # A tibble: 5 × 2
##   branch                   last_updated       
##   <chr>                    <dttm>             
## 1 25.0_debug-error         2024-02-13 04:29:39
## 2 25.1_debug-selected_vars 2024-01-15 10:29:25
## 3 25.2_debug-var_inputs    2024-01-15 10:25:12
## 4 25.3_debug-scatter_plot  2024-01-15 10:21:58
## 5 25.4_debug-print         2024-01-15 10:04:21

Launch the app:

launch(app = "23.1_debug-error")

Download the app:

get_app(app = "23.1_debug-error")

During regular development, Posit Workbench’s interactive debugger lets us inspect variables and expressions 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.

Launch app with the shinypak package:

launch('25.0_debug-error')

Let’s start by debugging the scatter plot in launch_app(). After loading, documenting and installing moviesApp, launch the application:


Ctrl/Cmd + Shift + L

launch_app(test = FALSE)
(a) Error in launch_app()
Figure 26.1: The Error messages in the UI is not always informative

The error printed in the UI is not very descriptive, but fortunately the following output is also printed to the Console:

Warning: Error in ggplot2::geom_point: Problem while computing aesthetics.
ℹ Error occurred in the 1st layer.
Caused by error in `.data[[NULL]]`:
! Must subset the data pronoun with a string, not `NULL`.

ggplot2 has excellent error messages: we can see the error is coming from ggplot2::geom_point(), specifically from one of the calls to .data. We can safely assume the source of this bug is the scatter_plot() utility function. However, we’ll proceed as if the message wasn’t very helpful or informative.

Debugging strategies

The two most common tools I use for debugging are:

  1. Wrapping browser() in a call to observe()
  2. Capturing reactive values with reactiveValuesToList() and sending output to the UI

These two methods cover 90% of my Shiny app debugging needs. In the following sections, I’ll provide examples of how–and when–I use each method.

browser()

browser() pauses code execution and activates the interactive debugger mode in the IDE, allowing us to view objects, execute code, and ‘step through’ each function line.

observe()

Shiny’s reactive model can make debugging challenging because the issues aren’t limited to logic or calculations but include the timing, sequence, or creation of reactive values. observe() creates a reactive observer that ‘listens’ for changes to reactive expressions (and executes code in response).

Wrapping browser() with observe() will trigger the debugger when the observer is invalidated, allowing us to interactively examine variables and reactive expressions (within the scope of the observe() function):

Posit Workbench’s interactive debugger
server <- function(input, output, session) {

    observe({
        browser()
    

    returned_values <- mod_something("bla")

    mod_something_else("blabla", input_values = returned_values)
    
    })
}
1
Shiny server function
2
observe() function scope
3
Call to browser() (called at the top of the observe() scope)
Debugging Reactivity in Shiny App-Packages

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

26.1 Debugging modules

The contents of your Shiny app-package can quickly become a complicated and intertwined combination of functions: utility, modules, UI, server, etc. I like to display the relationship between the functions with abstract syntax trees:2

For example, we know scatter_plot() is called from within the scatter plot display module function:

█─mod_scatter_display_server 
└─█─scatter_plot

And mod_scatter_display_server() is called within movies_server():

█─movies_server 
├─█─mod_scatter_display_server 
 └─█─scatter_plot 
└─█─mod_var_input_server 

Which is called from inside moviesApp():

█─launch_app 
├─█─movies_ui 
 ├─█─mod_var_input_ui 
 └─█─mod_scatter_display_ui 
└─█─movies_server 
  ├─█─mod_scatter_display_server 
   └─█─scatter_plot 
  └─█─mod_var_input_server

I find these abstract folder trees helpful when I’m debugging or testing Shiny functions. I can use them to try and anticipate the application call stack (especially when I end up with multiple utility functions or nested modules).

We’ll add browser() and observe() in the movies_server() function to capture the behaviors of both modules:

Launch app with the shinypak package:

launch('25.1_debug-selected_vars')
movies_server <- function(input, output, session) {

    observe({
      browser()
    
      selected_vars <- mod_var_input_server("vars")

      mod_scatter_display_server("plot", var_inputs = selected_vars)
      
    })

}
1
Observer scope
2
Activate debugger

Then we’ll load the package and display the app in the Viewer pane (below the Console):


Ctrl/Cmd + Shift + L

ℹ Loading moviesApp
launch_app(options = list(test.mode = FALSE), run = 'p')
ℹ shinyViewerType set to pane

The application launches, but browser() pauses the execution of the modules and activates the IDE’s debugger. This allows us to view the objects that are available in movies_server() before the variables are passed to the graph rendering functions:

(a) Debugger with call to browser() inside observe()
Figure 26.2: Note that the plot hasn’t rendered in the application yet because the call to observe(browser()) suspends the execution of any subsequent code

In the Source pane, we can see the call to browser() highlighted (Browse[1]> tells us the location in the browser() function).

(a) R/movies_server.R with observe(browser())
Figure 26.3: Because browser() was called inside observe(), the execution will pause, and we can interactively examine values

In the debugger, we want to confirm the returned values from the variable input module, selected_vars, which requires us to execute the next two lines of code:

Browse[1]> n
Browse[2]> n
(a) Execute the function line-by-line with n to create selected_vars
Figure 26.4: Click the Next icon twice to create selected_vars

Inside movies_server():

mod_var_input_server() collects the following values and returns a reactive list (selected_vars):

  • Three variable names
    • x, y, z
  • Graph aesthetics
    • alpha and size
  • An optional plot title
    • plot_title

When we inspect selected_vars in the debugger console (without parentheses) we see the method (i.e., the reactive list of inputs), and not the actual values:

Browse[2]> selected_vars
reactive({
    list(y = input$y, x = input$x,
         z = input$z, alpha = input$alpha, 
         size = input$size, 
         plot_title = input$plot_title)
})

If we check selected_vars() (with parentheses) in the debugger, we see this contains the values from the variable input module:

Browse[2]> selected_vars()
$y
[1] "audience_score"

$x
[1] "imdb_rating"

$z
[1] "mpaa_rating"

$alpha
[1] 0.5

$size
[1] 2

$plot_title
[1] ""

These two steps confirm that the UI values are being collected by the variable input module and stored in selected_vars, so the error must be coming from inside the scatter plot display module.

26.2 Module communication

Launch app with the shinypak package:

launch('25.2_debug-var_inputs')

We’ll repeat a similar process in mod_scatter_display_server(), but include the call to observe(browser()) after moduleServer(). Then we’ll load the package and run the application again:

mod_scatter_display_server <- function(id, var_inputs) {
  moduleServer(id, function(input, output, session) {
    
    observe({
      browser()
    
      # module code
      
      })

  })
}
1
Wrap browser() in observe() and place after the call to moduleServer()


Ctrl/Cmd + Shift + L

ℹ Loading moviesApp
launch_app(options = list(test.mode = FALSE), run = 'p')

Inside the module, we want to confirm var_inputs() is being created correctly from the var_inputs object in movies_server().

selected_vars is the input for mod_scatter_display_server() (as var_inputs)

  • var_inputs is converted to the reactive inputs
    • inputs is passed to scatter_plot() inside renderPlot()
Browse[2]> var_inputs()
$y
[1] "audience_score"

$x
[1] "imdb_rating"

$z
[1] "mpaa_rating"

$alpha
[1] 0.5

$size
[1] 2

$plot_title
[1] ""

26.2.1 Verify variable inputs

Inside the scatter plot display module, the var_inputs argument is used to create a reactive input() object for the graph created by scatter_plot():

    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
        )
    })
1
Variable inputs (from selected_vars)
2
inputs() for scatter_plot()

Now that we’ve confirmed var_inputs() has been created, we’ll verify the values are passed correctly from var_inputs() to inputs() (which is used to create the scatter plot).

To do this, we’ll progress through the module function (using n in the debugger console or by clicking Next) until the inputs() reactive has been created,

(a) Progressing past inputs() tells us it’s been created
Figure 26.5: Use n in the debugger or click Next to progress through the function
Browse[2]> inputs()
$x
[1] "imdb_rating"

$y
[1] "audience_score"

$z
[1] "mpaa_rating"

$alpha
[1] 0.5

$size
[1] 2

$plot_title
[1] ""

These two steps have shown us 1) the modules are communicating properly, and 2) the scatter plot display module contains the list of reactive values needed to render the graph.

My approach to debugging

If an application is producing a bug (i.e., failing to render an output, producing an error in the Console, etc.), I’ll start by placing a call to browser() (wrapped in observe()) at the top-level UI/server functions, then procced ‘down’ into the modules.

26.3 Debugging utility functions

Launch app with the shinypak package:

launch('25.3_debug-scatter_plot')

If we want to debug the scatter plot output, we need to move our observe(browser()) functions inside the call to renderPlot():

output$scatterplot <- renderPlot({
  observe({
    browser()

    
  })
})
1
Observe scope
2
Call to browser()

Load the package and run the application again:


Ctrl/Cmd + Shift + L

ℹ Loading moviesApp
launch_app(options = list(test.mode = FALSE), run = 'p')

Inside renderPlot(), we can progress to the creation of the plot object:

Browse[1]> n
debug at moviesApp/R/mod_scatter_display.R#68: 
  plot <- scatter_plot(
    df = movies, 
    x_var = inputs()$x, 
    y_var = inputs()$y, 
    col_var = inputs()$col, 
    alpha_var = inputs()$alpha, 
    size_var = inputs()$size)

From here we can step inside the scatter_plot() utility function to identify the source of the error:

Browse[2]> s
(a) Step into scatter_plot()
Figure 26.6: Use s in the debugger console to ‘step into’ scatter_plot()

Note the changes in the debugger console when we ‘step into’ scatter_plot():

debugging in: 
  scatter_plot(df = movies, 
    x_var = inputs()$x, 
    y_var = inputs()$y, 
    col_var = inputs()$col, 
    alpha_var = inputs()$alpha, 
    size_var = inputs()$size)
debug at /moviesApp/R/scatter_plot.R#30:
{   
    ggplot2::ggplot(data = df, 
      ggplot2::aes(x = .data[[x_var]], 
                   y = .data[[y_var]], 
                   color = .data[[col_var]])) + 
      ggplot2::geom_point(alpha = alpha_var, 
                          size = size_var)
}
1
Location of debugger in utility function

After some examination, we can identify the source of the error.

show/hide source of scatter_plot() bug
inputs <- reactive({
  plot_title <- tools::toTitleCase(var_inputs()$plot_title)
  list(
    x = var_inputs()$x,
    y = var_inputs()$y,
    col = var_inputs()$z,
    alpha = var_inputs()$alpha,
    size = var_inputs()$size,
    plot_title = plot_title
  )
})
plot <- scatter_plot(
  df = movies,
  x_var = inputs()$x,
  y_var = inputs()$y,
  col_var = inputs()$z,
  alpha_var = inputs()$alpha,
  size_var = inputs()$size
)
1
Color is assigned as col in inputs
2
Color is passed to scatter_plot() as col_var

26.4 Print debugging

Launch app with the shinypak package:

launch('25.4_debug-print')

One of the best tried and tested methods of debugging is simply adding a cat() or print() call somewhere in your code to print variables or objects to the R console. This is a basic but effective way to track variable changes.

We can do something similar in Shiny applications by combining verbatimTextOutput(), renderPrint(), and reactiveValuesToList():

In mod_var_input:

  • Place a verbatimTextOutput() in the ui function.
    code("module reactive values"),
    verbatimTextOutput(outputId = ns("mod_vals"))
  )
1
Optional label
2
Include the ns() for the inputId
  • In a renderPrint(), use reactiveValuesToList() to gather the inputIds and pass them to print() (I’m actually using lobstr::tree() to give a clearer display).
  output$mod_vals <- renderPrint({
    lobstr::tree(
      reactiveValuesToList(
        x = input,
        all.names = TRUE
      )
    )
  })
1
Collect reactive values in module
2
Print these values to the UI
3
Include all reactive objects

Load the package and run the app:


Ctrl/Cmd + Shift + L

ℹ Loading moviesApp
launch_app(options = list(test.mode = FALSE), run = 'p')
(a) ‘Print’ in launch_app()
Figure 26.7: reactiveValuesToList() printed from mod_var_inputs

Now we can see the reactive values from our module in the application sidebar!

We can also use this ‘print’ method to explore reactive values at various locations in our application. For example, if we wanted to print the reactive values for multiple modules in an app, we can use these methods in the top level movies_ui() and movies_server() functions.

In the bslib portion of movies_ui():

  • Add verbatimTextOutput() with an optional label

    code("reactive values"),
    verbatimTextOutput(outputId = 'vals')
    )

In movies_server():

  • Collect all the inputIds with reactiveValuesToList() and print with print() or lobstr::ast()

    all_vals <- reactive({
      reactiveValuesToList(x = input, all.names = TRUE)
    })
    
    output$vals <- renderPrint({
      lobstr::tree(all_vals())
    })

Load the package and run the app:


Ctrl/Cmd + Shift + L

ℹ Loading moviesApp
launch_app(options = list(test.mode = FALSE), 
  run = 'p', bslib = TRUE)
(a) ‘Print’ in launch_app(bslib = TRUE)
Figure 26.8: reactiveValuesToList() printed from movies_ui() and movies_server()

Here we can see both levels of reactive values (from the module and the UI/server functions). The handy thing about this method is that the values change when we interact with the application:

(a) Changing values in launch_app(bslib = TRUE)
Figure 26.9: y and vars-y both update when the UI inputs change

26.5 Exploring code

observe() and browser() aren’t only useful for addressing bugs–we can also use the debugger to understand how an application works.

Suppose it’s been awhile since we’ve looked at the ggplot2movies::movies application (run with ggp2_movies_app()). We can place calls to observe(browser()) in the inst/dev/app.R file, load the package, and run the application to see how it executes.

We’ll wrap the code in dev_mod_scatter_server() in observe(), and place a call to browser() before the alternate dataset is loaded:

dev_mod_scatter_server <- function(id, var_inputs) {
  moduleServer(id, function(input, output, session) {

    observe({
      browser()

    # load alternate data
    all_data <- fst::read_fst("tidy_movies.fst")




    })
  })
}
1
Observer scope
2
Call to browser()
3
Additional module code omitted

Load the package and run the app:


Ctrl/Cmd + Shift + L

ℹ Loading moviesApp
ggp2_movies_app(options = list(test.mode = FALSE), run = 'p')

This will suspend the execution of application before the tidy ggplot2movies::movies data are loaded (tidy_movies.fst), and before the graph is rendered:

Debugging dev_mod_scatter_server()

ggp2_movies_app() in interactive debugger

We can step through the module function to explore how the alternate data are loaded (and missing values are removed).

26.5.1 Verify app data

First we’ll proceed through the code until tidy_movies.fst is loaded, then confirm it’s structure:

Browse[2]> n
Browse[2]> n
Browse[2]> str(all_data)
'data.frame':   46002 obs. of  8 variables: 
 $ title     : chr  "'Til There Was You" ...
 $ length    : int  113 97 98 98 102 120 ...
 $ year      : int  1997 1999 2002 2004 ...
 $ budget    : int  23000000 16000000 ...
 $ avg_rating: num  4.8 6.7 5.6 6.4 6.1 ...
 $ votes     : int  799 19095 181 7859 ...
 $ mpaa      : Factor w/ 5 levels "G","PG" ...
 $ genre     : Factor w/ 8 levels "Action"...

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

26.5.2 Verfiy missing

After loading all_data, the module creates a reactive graph_data(). We can see graph_data() is ‘bound’ to input$missing, so we’ll confirm the input$missing value:

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

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

Browse[2]> n
Browse[2]> str(graph_data())
'data.frame':   1608 obs. of  8 variables: 
 $ title     : chr  "'Til There Was You" ...
 $ length    : int  113 97 98 98 102 120 ...
 $ year      : int  1997 1999 2002 2004 ...
 $ budget    : int  23000000 16000000 ...
 $ avg_rating: num  4.8 6.7 5.6 6.4 6.1 ...
 $ votes     : int  799 19095 181 7859 ...
 $ mpaa      : Factor w/ 5 levels "G","PG" ...
 $ genre     : Factor w/ 8 levels "Action"...

26.5.3 Verfiy variable inputs

Next we’ll confirm the var_inputs() list of graph values from our dev variable input module:

Browse[2]> str(var_inputs())
List of 6
 $ x         : chr "year"
 $ y         : chr "budget"
 $ z         : chr "mpaa"
 $ alpha     : num 0.4
 $ size      : num 2.5
 $ plot_title: chr ""

var_inputs() is creates the inputs() reactive for the graph, so we’ll confirm those values, too.

Browse[2]> n
Browse[2]> str(inputs())
List of 6
 $ x         : chr "year"
 $ y         : chr "budget"
 $ z         : chr "mpaa"
 $ alpha     : num 0.4
 $ size      : num 2.5
 $ plot_title: chr ""

26.5.4 Verfiy graph

Now that we have an understanding of the reactive flow inside the app, we’ll render the plot:

Browse[2]> n

Using browser() to ‘step through’ an application gives us a better understanding of the ‘order of execution’ in our dev scatter plot display module, (and it lets us see how input$missing and bindEvent() are working).

Recap

Recap: CI/CD!

browser() and observe() are powerful tools for debugging your application. Navigating a function using the debugger console gives you control over the execution of each line. If we want to see what’s happening ‘behind the scenes’, we can collect the reactive values and print them in the UI while the app is running.

The Shiny documentation also has a list of methods for debugging apps, and learning how to read call stacks (or a stacktrace) will help you debug your shiny app.3, 4

Please open an issue on GitHub


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

  2. Create abstract syntax trees with the ast() function from the lobstr package.↩︎

  3. Watch this video to learn about call stacks and abstract folder trees with lobstr.↩︎

  4. Stack traces are also covered in Advanced R, 2ed, Mastering Shiny, and in the Shiny documentation. I’ve summarized some tips on reading Shiny call stacks in the stack traces section on the Appendix.↩︎