Skip to content

Instantly share code, notes, and snippets.

@arraytools
Last active August 26, 2026 20:07
Show Gist options
  • Select an option

  • Save arraytools/c78a5962b616cd8a213ca4b896668e68 to your computer and use it in GitHub Desktop.

Select an option

Save arraytools/c78a5962b616cd8a213ca4b896668e68 to your computer and use it in GitHub Desktop.
A simple example illustrating how to add a logging system to an R shiny code using Shiny's options(shiny.error = ...) hook.

The log file is saved under the working directory.

What this does correctly

  • log_file and log_message() set up your logging function — nothing special here, just a helper that appends timestamped lines to a file.
  • options(shiny.error = ...) registers a global hook. Whenever an error occurs anywhere in your app's reactive code and nothing catches it, Shiny calls this function before showing its default error UI to the user.
  • geterrmessage() retrieves the text of whatever error condition was just raised — that's what gets written to the log.

Caveats to be aware of

  • Only errors, not warnings or messages. shiny.error fires only for stop()-class errors. If your code uses warning() or message(), those won't be logged with just this snippet — you'd need the withCallingHandlers() addition from earlier.
  • Only uncaught errors. If you (or a package you're using) wraps something in tryCatch() internally and handles the error there, shiny.error never fires, because the error never propagates up to Shiny's top level.
  • The user still sees Shiny's default error box. This snippet only handles logging — it doesn't change what's shown in the UI. The reactive block that errored stops executing at that point, so any output$... assignment after the error line won't run.
  • Global state. options(shiny.error = ...) is a single global setting for the whole app — you can't have different error-handling behavior for different parts of the app without additional logic inside that one function (e.g., checking session$token or similar).
  • Placement matters for multi-file apps. If you're using the ui.R / server.R two-file structure (rather than a single app.R), put these lines in a global.R file, or at the top of server.R before the server <- function(...) definition — anywhere that runs once when the app starts, before any reactive code fires.
library(shiny)
log_file <- file.path(getwd(), "shiny_app.log")
log_message <- function(msg, level = "INFO") {
entry <- sprintf("[%s] [%s] %s\n", format(Sys.time(), "%Y-%m-%d %H:%M:%S"), level, msg)
cat(entry, file = log_file, append = TRUE)
}
# Catches any uncaught error, anywhere in the app
options(shiny.error = function() {
log_message(geterrmessage(), level = "ERROR")
})
ui <- fluidPage(
titlePanel("Logging Example — website check"),
textInput("url", "Enter a URL to check:", value = "https://www.google.com"),
actionButton("go", "Check connection"),
textOutput("result")
)
server <- function(input, output, session) {
observeEvent(input$go, {
log_message(sprintf("Checking connection to: %s", input$url))
con <- url(input$url, open = "rb")
close(con)
log_message(sprintf("Successfully connected to: %s", input$url))
output$result <- renderText({
"Connection successful!"
})
})
}
shinyApp(ui, server)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment