Part 4 Errors and output

When using the future framework, it’s business as usual:

  • Errors produced in parallel, are relayed as-is in the main R session
  • Warnings produced in parallel, are relayed as-is in the main R session
  • Messages produced in parallel, are relayed as-is in the main R session
  • Any condition produced in parallel, are relayed as-is in the main R session
  • Standard output produced in parallel, is relayed as-is in the main R session

It is only the future framework that does this. Other parallel framework ignores warnings, messages, and standard output, and they throw their own custom error when they detect errors.

4.1 Business as usual: Exception handling (“dealing with errors”)

  • Errors produced in parallel, are relayed as-is in the main R session

4.1.1 Example setup

The regular sqrt() function gives a warning if we try with a negative number, e.g.

sqrt(-1)
#> [1] NaN
#> Warning message:
#> In sqrt(-1) : NaNs produced

If we want to be more strict, we could define:

strict_sqrt <- function(x) {
  if (x < 0) {
    stop("sqrt(x) with x < 0 not allowed: ", x)
  }
  sqrt(x)
}

which gives:

strict_sqrt(-1)
#> Error in strict_sqrt(-1) : sqrt(x) with x < 0 not allowed: -1

Let’s see how this behaves with futures:

f <- future(strict_sqrt(-1))

message("All good this far")
#> All good this far

v <- value(f)
#> Error in strict_sqrt(-1) : sqrt(x) with x < 0 not allowed: -1

Note how the error is signalled when we call value() - not when we call future(), which would not be possible because the latter starts the evaluation and returns immediately.

Because of this, regular exception handling applies, e.g.

v <- tryCatch({
  value(f)
}, error = function(e) {
  message("An error occurred: ", conditionMessage(e))
  NA_real_
})
#> An error occurred: sqrt(x) with x < 0 not allowed: -1
v
#> [1] NA

4.1.2 Exception handling works the same with map-reduce functions

Let’s see how this behaves with different map-reduce functions. If we use it with base R map-reduce functions, errors are produced like:

X <- -1:2
y <- lapply(X, strict_sqrt)
#> Error in FUN(X[[i]], ...) : sqrt(x) with x < 0 not allowed: -1

We get a similar behavior with future.apply:

library(future.apply)
plan(multisession, workers = 2)

y <- future_lapply(X, strict_sqrt)
#> Error in ...future.FUN(...future.X_jj, ...) : 
#>   sqrt(x) with x < 0 not allowed: -1

and furrr:

library(furrr)
plan(multisession, workers = 2)

y <- future_map(X, strict_sqrt)
#> Error in ...furrr_fn(...) : sqrt(x) with x < 0 not allowed: -1

Take-home message: You can treat errors the same way as when running sequentially.

See Appendix Exception handling by other parallel map-reduce APIs for a comparison with other parallel solutions that does not rely on future-based, and for why the future solution is often more natural.

4.2 Business as usual: Warnings

  • Warnings produced in parallel, are relayed as-is in the main R session

When you use warning(), the warning message is signalled as a warning condition, very similar to how errors are signalled as error conditions. For example,

x <- -1:2
y <- log(x)
#> Warning message:
#> In log(x) : NaNs produced

Futures capture warning conditions, which then are re-signalled by value(), e.g.

x <- -1:2
f <- future(log(x))
y <- value(f)
#> Warning message:
#> In log(x) : NaNs produced

We can suppress warning using suppressWarnings(). This works the same way regardless whether futures are used or not. For example,

y <- value(f)
#> Warning message:
#> In log(x) : NaNs produced

suppressWarnings(y <- value(f))

One can handle warning conditions using withCallingHandlers() and globalCallingHandlers() for maximum control what to do with them, but that’s beyond this tutorial.

Just like errors, future map-reduce functions handles warnings consistently, e.g.

library(future.apply)
plan(multisession, workers = 2)

X <- -1:2
y <- future_lapply(X, sqrt)
#> Warning message:
#> In ...future.FUN(...future.X_jj, ...) : NaNs produced
str(y)
#> List of 4
#>  $ : num NaN
#>  $ : num 0
#>  $ : num 1
#>  $ : num 1.41

Take-home message: You can treat warnings the same way as when running sequentially.

See Appendix Condition handling by other parallel map-reduce APIs for a comparison with other parallel solutions. The future framework is the only solution where it works.

4.3 Business as usual: Messages

  • Messages produced in parallel, are relayed as-is in the main R session

When you use message(), the message are signalled the same way warnings and errors are signalled, and they eventually end up in the standard error (stderr) stream - not outputted to the standard output (stdout) stream. For example,

z <- letters[1:8]
message("Number of letters: ", length(z))
#> Number of letters: 8

As with warnings and errors, futures capture message conditions, which then are re-signalled by value(), e.g.

f <- future({
  z <- letters[1:8]
  message("Number of letters: ", length(z))
  z
})
y <- value(f)
#> Number of letters: 8
y
#> [1] "a" "b" "c" "d" "e" "f" "g" "h"

We can suppress messages using suppressMessages(). This works the same way regardless whether futures are used or not. For example,

y <- value(f)
#> Number of letters: 8

suppressMessages(y <- value(f))

Just like for warnings and errors, future map-reduce functions handles messages consistently, e.g.

library(future.apply)
plan(multisession, workers = 2)

X <- 1:3
y <- future_lapply(X, function(x) message("x = ", x))
x = 1
x = 2
x = 3

One can handle message conditions using withCallingHandlers() and globalCallingHandlers() for maximum control what to do with them, but that’s beyond this tutorial.

Take-home message: You can treat messages the same way as when running sequentially.

See Appendix Condition handling by other parallel map-reduce APIs for a comparison with other parallel solutions. The future framework is the only solution where it works.

4.4 Business as usual: Standard output

  • Standard output produced in parallel, is relayed as-is in the main R session

When you use cat(), print(), and str(), the message string is (by default) outputted to the standard output (stdout) stream. Note that this does not rely on R’s condition system, so use a completely different infrastructure than message().

For example,

z <- letters[1:8]
cat("Number of letters:", length(z), "\n")
#> Number of letters: 8

When using futures, any standard output is automatically captured and re-outputted by value(), e.g.

f <- future({
  z <- letters[1:8]
  cat("Number of letters:", length(z), "\n")
  z
})
y <- value(f)
#> Number of letters: 8
y
#> [1] "a" "b" "c" "d" "e" "f" "g" "h"

Just like you can use utils::capture.output() to capture standard output from cat(), print(), str(), …, you can use it to capture the output relayed by value(). For example,

stdout <- capture.output({
  y <- value(f)
})
y
#> [1] "a" "b" "c" "d" "e" "f" "g" "h"
stdout
#> [1] "Number of letters: 8 "

We can suppress standard output using capture.output(..., file = nullfile()). This works the same way regardless whether futures are used or not. For example,

y <- value(f)
#> Number of letters: 8

capture.output(y <- value(f), file = nullfile())

Just like for conditions, future map-reduce functions handles standard ouput consistently, e.g.

library(future.apply)
plan(multisession, workers = 2)

X <- 1:3
y <- future_lapply(X, function(x) cat("x =", x, "\n"))
x = 1
x = 2
x = 3

Take-home message: You can treat output the same way as when running sequentially.

See Appendix Standard output by other parallel map-reduce APIs for a comparison with other parallel solutions that does not rely on future-based. The future-based solutions are the only ones that work correctly.

4.5 Summary: All types of output is relayed

Above, we’ve learned how message, warning, and error conditions are relayed. This is actually true for all other classes of conditions. We also learned that output sent to the standard output is relayed by futures.

Here is a final example where we use most types of output:

f <- future({
  cat("Hello world\n")
  message("Hi there")
  warning("whoops!")
  message("Please wait ...")
  log("a")
  message("Done")
})

value(f)
#> Hello world
#> Hi there
#> Please wait ...
#> Error in log("a") : non-numeric argument to mathematical function
#> In addition: Warning message:
#> In withCallingHandlers({ : whoops!

4.6 Odds and ends

4.6.1 What about standard error (stderr)?

R’s support for capturing standard error (stderr) output is poor. It can be done using:

capture.output(..., type = "message")

However, I strongly advise against using it. The reason is that capture.output(..., type = "message") cannot be nested, and the most recent call will always trump any existing stderr captures. That is, if you capture standard error this way and call a function that does the same, the latter hijacks all capturing. Importantly, when it returns, remaining output to standard error will no longer be captured, despite your having requested it previously. For more details, see https://github.com/HenrikBengtsson/Wishlist-for-R/issues/55.

Conclusion: Always avoid capture.output(..., type = "message"), and never ever use it in package code!

Warning: Above type = "message" should not be mistaken for message(). A more informative value would have been type = "stderr".