diff --git a/NEWS.md b/NEWS.md index b693d0f8f..68043caa4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -23,6 +23,8 @@ * Shiny's Typescript assets are now compiled to ES2021 instead of ES5. (#4066) +* `ExtendedTask` now catches synchronous values and errors and returns them via `$result()`. Previously, the extended task function was required to always return a promise. This change makes it easier to use `ExtendedTask` with a function that may return early or do some synchronous work before returning a promise. (#4225) + ## Bug fixes * Fixed a bug with modals where calling `removeModal()` too quickly after `showModal()` would fail to remove the modal if the remove modal message was received while the modal was in the process of being revealed. (#4173) @@ -31,6 +33,8 @@ * Updated the JavaScript used when inserting a tab to avoid rendering dynamic UI elements twice when adding the new tab via `insertTab()` or `bslib::nav_insert()`. (#4179) +* Fixed an issue with `ExtendedTask` where synchronous errors would cause an error that would stop the current session. (#4225) + # shiny 1.10.0 ## New features and improvements diff --git a/R/extended-task.R b/R/extended-task.R index e29e78daf..131251bb9 100644 --- a/R/extended-task.R +++ b/R/extended-task.R @@ -130,14 +130,15 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE, #' arguments. invoke = function(...) { args <- rlang::dots_list(..., .ignore_empty = "none") + call <- rlang::caller_call(n = 0) if ( isolate(private$rv_status()) == "running" || private$invocation_queue$size() > 0 ) { - private$invocation_queue$add(args) + private$invocation_queue$add(list(args = args, call = call)) } else { - private$do_invoke(args) + private$do_invoke(args, call = call) } invisible(NULL) }, @@ -204,44 +205,41 @@ ExtendedTask <- R6Class("ExtendedTask", portable = TRUE, cloneable = FALSE, rv_error = NULL, invocation_queue = NULL, - do_invoke = function(args) { + do_invoke = function(args, call = NULL) { private$rv_status("running") private$rv_value(NULL) private$rv_error(NULL) - p <- NULL - tryCatch({ - maskReactiveContext({ - # TODO: Bounce the do.call off of a promise_resolve(), so that the - # call to invoke() always returns immediately? - result <- do.call(private$func, args) - p <- promises::as.promise(result) - }) - }, error = function(e) { - private$on_error(e) - }) + p <- promises::promise_resolve( + maskReactiveContext(do.call(private$func, args)) + ) - promises::finally( - promises::then(p, - onFulfilled = function(value, .visible) { - private$on_success(list(value=value, visible=.visible)) - }, - onRejected = function(error) { - private$on_error(error) - } - ), - onFinally = function() { - if (private$invocation_queue$size() > 0) { - private$do_invoke(private$invocation_queue$remove()) - } + p <- promises::then( + p, + onFulfilled = function(value, .visible) { + private$on_success(list(value = value, visible = .visible)) + }, + onRejected = function(error) { + private$on_error(error, call = call) } ) + promises::finally(p, onFinally = function() { + if (private$invocation_queue$size() > 0) { + next_call <- private$invocation_queue$remove() + private$do_invoke(next_call$args, next_call$call) + } + }) invisible(NULL) }, - on_error = function(err) { + on_error = function(err, call = NULL) { + cli::cli_warn( + "ERROR: An error occurred when invoking the ExtendedTask.", + parent = err, + call = call + ) private$rv_status("error") private$rv_error(err) },