I want to prevent my app from crashing in the case of an unforseen error. This happens when errors happen inside observers, so I created custom observer functions that handle errors by displaying the error message as a shiny popup. However, I can't seem to get the stack trace. I see for each error only calls that are internal to shiny.
This is probably because the custom observer quotes and then eval()s the code.. How can I get an error message that points me to code that I have written and possibly even avoid the shiny stack trace that is always the same?
This is what I have so far:
library(shiny)
library(shinyjs)
# Pass through silent shiny "errors" (used for skipping observers by req())
# and show popup if it is a "real" error.
handle_errors_except_shiny <- function(error){
if(inherits(error, "shiny.silent.error")){
stop(error)
}else{
show_error_popup(error)
}
}
# like shiny::observe(), but display a notification in case of error
safe_observe <- function(x, env = parent.frame(), quoted = FALSE, ..., domain = getDefaultReactiveDomain()){
if(!quoted){
x <- substitute(x)
}
x <- bquote(tryCatch( .(x), error = handle_errors_except_shiny ))
observe(x=x, env=env, quoted=TRUE, domain=domain, ...)
}
# Can be used as error handler in tryCatch( error= ... ) within observers to prevent application crash but
# rather show a message
#
# If the error message has the class "userError", a stack trace is not printed but
# just the error text
# This allows to use this function to display errors that are meant to guide the user.
show_error_popup <- function(e, top_frame = NULL, bottom_frame = parent.frame()){
stacktrace <- rlang::trace_back(top = top_frame, bottom = bottom_frame)
showNotification(conditionMessage(e), type = "error", duration = NULL)
# Print stack trace but it contains only shiny-internal functions :-(
print(stacktrace)
}
ui <- fluidPage(
actionButton("btn", "Click Me")
)
server <- function(input, output, session) {
safe_observe({
req(input$btn) # take dependency on button
handle_button()
})
}
handle_button <- function(){
stop("test error")
message("Button clicked\n")
}
shinyApp(ui, server)
Output has no reference to handle_button
and is unnecessarily complex to help with debugging the app:
▆
1. ├─base (local) `<fn>`(x)
2. └─shiny:::print.shiny.appobj(x)
3. └─shiny::runApp(x) at shiny/R/shinyapp.R:565:3
4. ├─shiny::..stacktraceoff..(...) at shiny/R/runapp.R:388:3
5. ├─shiny::captureStackTraces(...) at shiny/R/runapp.R:388:3
6. │ └─promises::with_promise_domain(...) at shiny/R/conditions.R:125:3
7. │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
8. │ └─base::withCallingHandlers(expr, error = doCaptureStack) at shiny/R/conditions.R:185:7
9. ├─shiny:::..stacktracefloor..(serviceApp()) at shiny/R/runapp.R:391:9
10. └─shiny:::serviceApp() at shiny/R/runapp.R:391:9
11. └─shiny:::flushReact() at shiny/R/server.R:492:3
12. └─.getReactiveEnvironment()$flush() at shiny/R/react.R:200:3
13. └─ctx$executeFlushCallbacks() at shiny/R/react.R:180:9
14. └─base::lapply(...) at shiny/R/react.R:111:7
15. └─shiny (local) FUN(X[[i]], ...)
16. └─shiny (local) flushCallback() at shiny/R/react.R:112:9
17. ├─shiny:::hybrid_chain(...) at shiny/R/reactives.R:1204:9
18. │ └─shiny (local) do() at shiny/R/utils.R:1558:5
19. │ ├─base::tryCatch(...) at shiny/R/utils.R:1518:5
20. │ │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
21. │ │ └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
22. │ │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
23. │ ├─shiny::captureStackTraces(...) at shiny/R/utils.R:1520:9
24. │ │ └─promises::with_promise_domain(...) at shiny/R/conditions.R:125:3
25. │ │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
26. │ │ └─base::withCallingHandlers(expr, error = doCaptureStack) at shiny/R/conditions.R:185:7
27. │ ├─base::withVisible(force(expr)) at shiny/R/utils.R:1521:11
28. │ └─base::force(expr) at shiny/R/utils.R:1521:11
29. ├─shiny:::shinyCallingHandlers(run()) at shiny/R/reactives.R:1207:15
30. │ ├─base::withCallingHandlers(...) at shiny/R/utils.R:484:3
31. │ └─shiny::captureStackTraces(expr) at shiny/R/utils.R:484:3
32. │ └─promises::with_promise_domain(...) at shiny/R/conditions.R:125:3
33. │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
34. │ └─base::withCallingHandlers(expr, error = doCaptureStack) at shiny/R/conditions.R:185:7
35. └─shiny (local) run() at shiny/R/utils.R:484:3
36. └─ctx$run(.func) at shiny/R/reactives.R:1234:7
37. ├─promises::with_promise_domain(...) at shiny/R/react.R:54:7
38. │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
39. ├─shiny::withReactiveDomain(...) at shiny/R/react.R:55:9
40. │ └─promises::with_promise_domain(...) at shiny/R/reactive-domains.R:98:3
41. │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
42. │ └─base::force(expr) at shiny/R/utils.R:1598:7
43. └─env$runWith(self, func) at shiny/R/react.R:59:11
44. └─shiny (local) contextFunc() at shiny/R/react.R:158:7
45. ├─shiny::..stacktraceon..(`<observer>`(...)) at shiny/R/utils.R:1449:22
46. └─shiny (local) `<observer>`(...) at shiny/R/conditions.R:522:21
47. └─observe() at shiny/R/utils.R:1459:22
48. └─base::tryCatch(...)
49. └─base (local) tryCatchList(expr, classes, parentenv, handlers)
50. └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
51. └─value[[3L]](cond)
I want to prevent my app from crashing in the case of an unforseen error. This happens when errors happen inside observers, so I created custom observer functions that handle errors by displaying the error message as a shiny popup. However, I can't seem to get the stack trace. I see for each error only calls that are internal to shiny.
This is probably because the custom observer quotes and then eval()s the code.. How can I get an error message that points me to code that I have written and possibly even avoid the shiny stack trace that is always the same?
This is what I have so far:
library(shiny)
library(shinyjs)
# Pass through silent shiny "errors" (used for skipping observers by req())
# and show popup if it is a "real" error.
handle_errors_except_shiny <- function(error){
if(inherits(error, "shiny.silent.error")){
stop(error)
}else{
show_error_popup(error)
}
}
# like shiny::observe(), but display a notification in case of error
safe_observe <- function(x, env = parent.frame(), quoted = FALSE, ..., domain = getDefaultReactiveDomain()){
if(!quoted){
x <- substitute(x)
}
x <- bquote(tryCatch( .(x), error = handle_errors_except_shiny ))
observe(x=x, env=env, quoted=TRUE, domain=domain, ...)
}
# Can be used as error handler in tryCatch( error= ... ) within observers to prevent application crash but
# rather show a message
#
# If the error message has the class "userError", a stack trace is not printed but
# just the error text
# This allows to use this function to display errors that are meant to guide the user.
show_error_popup <- function(e, top_frame = NULL, bottom_frame = parent.frame()){
stacktrace <- rlang::trace_back(top = top_frame, bottom = bottom_frame)
showNotification(conditionMessage(e), type = "error", duration = NULL)
# Print stack trace but it contains only shiny-internal functions :-(
print(stacktrace)
}
ui <- fluidPage(
actionButton("btn", "Click Me")
)
server <- function(input, output, session) {
safe_observe({
req(input$btn) # take dependency on button
handle_button()
})
}
handle_button <- function(){
stop("test error")
message("Button clicked\n")
}
shinyApp(ui, server)
Output has no reference to handle_button
and is unnecessarily complex to help with debugging the app:
▆
1. ├─base (local) `<fn>`(x)
2. └─shiny:::print.shiny.appobj(x)
3. └─shiny::runApp(x) at shiny/R/shinyapp.R:565:3
4. ├─shiny::..stacktraceoff..(...) at shiny/R/runapp.R:388:3
5. ├─shiny::captureStackTraces(...) at shiny/R/runapp.R:388:3
6. │ └─promises::with_promise_domain(...) at shiny/R/conditions.R:125:3
7. │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
8. │ └─base::withCallingHandlers(expr, error = doCaptureStack) at shiny/R/conditions.R:185:7
9. ├─shiny:::..stacktracefloor..(serviceApp()) at shiny/R/runapp.R:391:9
10. └─shiny:::serviceApp() at shiny/R/runapp.R:391:9
11. └─shiny:::flushReact() at shiny/R/server.R:492:3
12. └─.getReactiveEnvironment()$flush() at shiny/R/react.R:200:3
13. └─ctx$executeFlushCallbacks() at shiny/R/react.R:180:9
14. └─base::lapply(...) at shiny/R/react.R:111:7
15. └─shiny (local) FUN(X[[i]], ...)
16. └─shiny (local) flushCallback() at shiny/R/react.R:112:9
17. ├─shiny:::hybrid_chain(...) at shiny/R/reactives.R:1204:9
18. │ └─shiny (local) do() at shiny/R/utils.R:1558:5
19. │ ├─base::tryCatch(...) at shiny/R/utils.R:1518:5
20. │ │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
21. │ │ └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
22. │ │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
23. │ ├─shiny::captureStackTraces(...) at shiny/R/utils.R:1520:9
24. │ │ └─promises::with_promise_domain(...) at shiny/R/conditions.R:125:3
25. │ │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
26. │ │ └─base::withCallingHandlers(expr, error = doCaptureStack) at shiny/R/conditions.R:185:7
27. │ ├─base::withVisible(force(expr)) at shiny/R/utils.R:1521:11
28. │ └─base::force(expr) at shiny/R/utils.R:1521:11
29. ├─shiny:::shinyCallingHandlers(run()) at shiny/R/reactives.R:1207:15
30. │ ├─base::withCallingHandlers(...) at shiny/R/utils.R:484:3
31. │ └─shiny::captureStackTraces(expr) at shiny/R/utils.R:484:3
32. │ └─promises::with_promise_domain(...) at shiny/R/conditions.R:125:3
33. │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
34. │ └─base::withCallingHandlers(expr, error = doCaptureStack) at shiny/R/conditions.R:185:7
35. └─shiny (local) run() at shiny/R/utils.R:484:3
36. └─ctx$run(.func) at shiny/R/reactives.R:1234:7
37. ├─promises::with_promise_domain(...) at shiny/R/react.R:54:7
38. │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
39. ├─shiny::withReactiveDomain(...) at shiny/R/react.R:55:9
40. │ └─promises::with_promise_domain(...) at shiny/R/reactive-domains.R:98:3
41. │ └─domain$wrapSync(expr) at promises/R/domains.R:171:3
42. │ └─base::force(expr) at shiny/R/utils.R:1598:7
43. └─env$runWith(self, func) at shiny/R/react.R:59:11
44. └─shiny (local) contextFunc() at shiny/R/react.R:158:7
45. ├─shiny::..stacktraceon..(`<observer>`(...)) at shiny/R/utils.R:1449:22
46. └─shiny (local) `<observer>`(...) at shiny/R/conditions.R:522:21
47. └─observe() at shiny/R/utils.R:1459:22
48. └─base::tryCatch(...)
49. └─base (local) tryCatchList(expr, classes, parentenv, handlers)
50. └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
51. └─value[[3L]](cond)
Share
Improve this question
edited 2 days ago
akraf
asked 2 days ago
akrafakraf
3,23526 silver badges49 bronze badges
1 Answer
Reset to default 0I found that shiny has internal functions to handle this problem. You get what you want by changing the following functions:
# like shiny::observe(), but display a notification in case of error
safe_observe <- function(x, env = parent.frame(), quoted = FALSE, ..., domain = getDefaultReactiveDomain()){
if(!quoted){
x <- substitute(x)
}
# >>>>>> add captureStackTraces() here <<<<<<<<<<<<<<<<<<<<<<<<<<<<
x <- bquote(tryCatch( captureStackTraces(.(x)), error = handle_errors_except_shiny ))
observe(x=x, env=env, quoted=TRUE, domain=domain, ...)
}
# Can be used as error handler in tryCatch( error= ... ) within observers to prevent application crash but
# rather show a message
#
# If the error message has the class "userError", a stack trace is not printed but
# just the error text
# This allows to use this function to display errors that are meant to guide the user.
show_error_popup <- function(e, top_frame = NULL, bottom_frame = parent.frame()){
stacktrace <- rlang::trace_back(top = top_frame, bottom = bottom_frame)
# >>> The function prints the stack trace to the console, you want to capture it <<<<<
t <- capture.output(printStackTrace(e),type = "message")
# >>> convert the formatting from terminal color codes to HTML <<<<<<<<
t <- paste(cli::ansi_html(t),collapse="\n")
showNotification(HTML(paste0("<div>",conditionMessage(e),"</div><pre>", t, "</pre>")),
type = "error", duration = NULL)
}
This shows handle_button
in the stack trace as desired.