--- title: "Result and Error Handling" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Result and Error Handling} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```r library(jobqueue) q <- Queue$new() ``` ## Reacting to Results ```r my_job <- q$run({ 42 }) # === A 'jobqueue'-style callback ============ my_job$on('done', ~message('Result = ', .$result)) #> Result = 42 # === A 'promises'-style callback ============ my_job %...>% message('Result = ', .) #> Result = 42 ``` See also `vignette('hooks')` ## Output vs Result When `expr` is finished evaluating, the result is assigned to `$output`. ```r job <- q$run({ 42 }) job$output #> [1] 42 ``` By default, `$result` will return the exact same value as `$output`. ```r job$result #> [1] 42 ``` However, while `$output` is a fixed value, `$result` is highly configurable. Most notably by the `reformat` and `signal` parameters. ## Reformatting Output Use `reformat = function (job) {...}` to define what should be returned by `$result`. > **Important** > > You may access `job$output` in your `reformat` function. > DO NOT access `job$result`, which would start an infinite recusion. `reformat` can be set in several places: * `Queue$new(reformat = http_wrap)` * `Job$new(reformat = http_wrap)` * `$run(reformat = http_wrap)` * `$reformat <- http_wrap` ### Transformation ```r http_wrap <- function (job) { result <- job$output paste0('{"status":200,"body":{"result":', result, '}}') } job <- q$run({ 42 }, reformat = http_wrap) cat(job$result) #> {"status":200,"body":{"result":42}} ``` ### Job Details ```r with_id <- function (job) { list(result = job$output, id = job$id) } job <- q$run({ 42 }, reformat = with_id, id = 'abc') dput(job$result) #> list(result = 42, id = "abc") ``` ### Non-blocking `$output` blocks until the Job is done. If you want `$result` to not block, you can return a placeholder value instead. ```r reformat <- function (job) { if (job$is_done) { job$output$result } else { 'result not available yet' } } job <- q$run({ Sys.sleep(5); 42 }, reformat = reformat) job$result #> [1] "result not available yet" job$state #> [1] "running" ``` ## Signaling Errors By default, errors and interrupts will be caught by jobqueue and returned as a condition object from `$result`. ```r expr <- quote(stop('error XYZ')) job <- q$run(expr) x <- job$result inherits(x, 'condition') #> TRUE x #> Error in `q$run(expr)`: #> ! Error evaluating $expr on background process #> Caused by error in `stop("error XYZ")`: #> ! error XYZ #> --- #> Subprocess backtrace: #> 1. base::eval(expr = expr, envir = vars, enclos = .GlobalEnv) #> 2. base::stop("error XYZ") #> 3. | base::.handleSimpleError(function (e) … #> 4. global h(simpleError(msg, call)) ``` If you want those errors to continue propagating, set `signal = TRUE`. ```r job <- q$run(expr, signal = TRUE) x <- job$result #> Error in "q$run(expr, signal = TRUE)" : #> ! Error evaluating $expr on background process #> Caused by error in `stop("error XYZ")`: #> ! error XYZ ``` These signals can be caught as usual by `try()` and `tryCatch()`. ```r print_cnd <- function (cnd) cli::cli_text('Caught {.type {cnd}}.') job <- q$run({ Sys.sleep(5) }, signal = TRUE)$stop() res <- tryCatch(job$result, error = print_cnd, interrupt = print_cnd) #> Caught an object. res #> NULL job <- q$run({ stop('error XYZ') }, signal = TRUE) res <- tryCatch(job$result, error = print_cnd, interrupt = print_cnd) #> Caught an object. res #> NULL ``` You can also signal some conditions while catching others. Perhaps stopping a job is part of the "normal" workflow and shouldn't be lumped in with other errors. ```r job <- q$run({ Sys.sleep(5) }, signal = 'error')$stop() res <- tryCatch(job$result, error = print_cnd) res #> job <- q$run({ stop('error XYZ') }, signal = 'error') res <- tryCatch(job$result, error = print_cnd) #> Caught an object. res #> NULL ``` ## Promise Handling You can pass a Job directly to any function that expects a 'promise' object, such as `promises::then` or `promises::%...>%`. These functions are re-exported by jobqueue, so calling `library(promises)` is optional. ```r q$run({ 42 }) %...>% message #> 42 ``` By default, errors and interrupts are caught by jobqueue and used for promise fulfillment. ```r onFulfilled <- function (value) cli::cli_text('Fulfilled with {.type {value}}.') onRejected <- function (err) cli::cli_text('Rejected with {.type {err}}.') job <- q$run({ Sys.sleep(5) })$stop() job %>% then(onFulfilled, onRejected) #> Fulfilled with an object. job <- q$run({ stop('error XYZ') }) job %>% then(onFulfilled, onRejected) #> Fulfilled with an object. ``` Setting `signal = TRUE` will cause errors and interrupts be sent to the promise's rejection handler instead. ```r job <- q$run({ Sys.sleep(5) }, signal = TRUE)$stop() job %>% then(onFulfilled, onRejected) #> Rejected with an object. job <- q$run({ stop('error XYZ') }, signal = TRUE) job %>% then(onFulfilled, onRejected) #> Rejected with an object. ```