Skip to content

Commit e2b7f91

Browse files
authored
feat: Add shiny.error.unhandled error handler (#3989)
* feat(shiny.error.unhandled): Allow users to provide an unhandled error handler * Extract `shinyUserErrorUnhandled()` to use in MockSession too * tests(shiny.error.unhandled): Test that unhandled errors are handled safely * docs: Clarify that session still ends with an unhandled error * docs: Add news item
1 parent c73978c commit e2b7f91

File tree

7 files changed

+88
-0
lines changed

7 files changed

+88
-0
lines changed

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
* Added a new `ExtendedTask` abstraction, for long-running asynchronous tasks that you don't want to block the rest of the app, or even the rest of the session. Designed to be used with new `bslib::input_task_button()` and `bslib::bind_task_button()` functions that help give user feedback and prevent extra button clicks. (#3958)
1212

13+
* Added a `shiny.error.unhandled` option that can be set to a function that will be called when an unhandled error occurs in a Shiny app. Note that this handler doesn't stop the error or prevent the session from closing, but it can be used to log the error or to clean up session-specific resources. (thanks @JohnCoene, #3989)
14+
1315
## Bug fixes
1416

1517
* Notifications are now constrained to the width of the viewport for window widths smaller the default notification panel size. (#3949)

R/mock-session.R

+1
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ MockShinySession <- R6Class(
563563
#' @description Called by observers when a reactive expression errors.
564564
#' @param e An error object.
565565
unhandledError = function(e) {
566+
shinyUserErrorUnhandled(e)
566567
self$close()
567568
},
568569
#' @description Freeze a value until the flush cycle completes.

R/shiny-options.R

+6
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ getShinyOption <- function(name, default = NULL) {
8181
#' \item{shiny.error (defaults to `NULL`)}{This can be a function which is called when an error
8282
#' occurs. For example, `options(shiny.error=recover)` will result a
8383
#' the debugger prompt when an error occurs.}
84+
#' \item{shiny.error.unhandled (defaults to `NULL`)}{A function that will be
85+
#' called when an unhandled error that will stop the app session occurs. This
86+
#' function should take the error condition object as its first argument.
87+
#' Note that this function will not stop the error or prevent the session
88+
#' from ending, but it will provide you with an opportunity to log the error
89+
#' or clean up resources before the session is closed.}
8490
#' \item{shiny.fullstacktrace (defaults to `FALSE`)}{Controls whether "pretty" (`FALSE`) or full
8591
#' stack traces (`TRUE`) are dumped to the console when errors occur during Shiny app execution.
8692
#' Pretty stack traces attempt to only show user-supplied code, but this pruning can't always

R/shiny.R

+2
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,8 @@ ShinySession <- R6Class(
10441044
return(private$inputReceivedCallbacks$register(callback))
10451045
},
10461046
unhandledError = function(e) {
1047+
"Call the user's unhandled error handler and then close the session."
1048+
shinyUserErrorUnhandled(e)
10471049
self$close()
10481050
},
10491051
close = function() {

R/utils.R

+24
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,30 @@ shinyCallingHandlers <- function(expr) {
493493
)
494494
}
495495

496+
shinyUserErrorUnhandled <- function(error, handler = NULL) {
497+
if (is.null(handler)) {
498+
handler <- getShinyOption(
499+
"shiny.error.unhandled",
500+
getOption("shiny.error.unhandled", NULL)
501+
)
502+
}
503+
504+
if (is.null(handler)) return()
505+
506+
if (!is.function(handler) || length(formals(handler)) == 0) {
507+
warning(
508+
"`shiny.error.unhandled` must be a function ",
509+
"that takes an error object as its first argument",
510+
immediate. = TRUE
511+
)
512+
return()
513+
}
514+
515+
tryCatch(
516+
shinyCallingHandlers(handler(error)),
517+
error = printError
518+
)
519+
}
496520

497521
#' Register a function with the debugger (if one is active).
498522
#'

man/shinyOptions.Rd

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-test-server.R

+47
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,53 @@ test_that("session ended handlers work", {
507507
})
508508
})
509509

510+
test_that("shiny.error.unhandled handles unhandled errors", {
511+
caught <- NULL
512+
op <- options(shiny.error.unhandled = function(error) {
513+
caught <<- error
514+
stop("bad user error handler")
515+
})
516+
on.exit(options(op))
517+
518+
server <- function(input, output, session) {
519+
observe({
520+
req(input$boom > 1) # This signals an error that shiny handles
521+
stop("unhandled error") # This error is *not* and brings down the app
522+
})
523+
}
524+
525+
testServer(server, {
526+
session$setInputs(boom = 1)
527+
# validation errors *are* handled and don't trigger the unhandled error
528+
expect_null(caught)
529+
expect_false(session$isEnded())
530+
expect_false(session$isClosed())
531+
532+
# All errors are caught, even the error from the unhandled error handler
533+
# And these errors are converted to warnings
534+
expect_no_error(
535+
expect_warning(
536+
expect_warning(
537+
# Setting input$boom = 2 throws two errors that become warnings:
538+
# 1. The unhandled error in the observe
539+
# 2. The error thrown by the user error handler
540+
capture.output(
541+
session$setInputs(boom = 2),
542+
type = "message"
543+
),
544+
"unhandled error"
545+
),
546+
"bad user error handler"
547+
)
548+
)
549+
550+
expect_s3_class(caught, "error")
551+
expect_equal(conditionMessage(caught), "unhandled error")
552+
expect_true(session$isEnded())
553+
expect_true(session$isClosed())
554+
})
555+
})
556+
510557
test_that("session flush handlers work", {
511558
server <- function(input, output, session) {
512559
rv <- reactiveValues(x = 0, flushCounter = 0, flushedCounter = 0,

0 commit comments

Comments
 (0)