R class try error

try is a wrapper to run an expression that might fail and allow the user's code to handle error-recovery.

try: Try an Expression Allowing Error Recovery

Description

try is a wrapper to run an expression that might fail and allow
the user’s code to handle error-recovery.

Usage

try(expr, silent = FALSE,
    outFile = getOption("try.outFile", default = stderr()))

Arguments

expr

an R expression to try.

silent

logical: should the report of error messages be
suppressed?

outFile

a connection, or a character string naming the
file to print to (via cat(*, file = outFile));
used only if silent is false, as by default.

Value

The value of the expression if expr is evaluated without error,
but an invisible object of class "try-error" containing the
error message, and the error condition as the "condition"
attribute, if it fails.

Details

try evaluates an expression and traps any errors that occur
during the evaluation. If an error occurs then the error
message is printed to the stderr connection unless
options("show.error.messages") is false or
the call includes silent = TRUE. The error message is also
stored in a buffer where it can be retrieved by
geterrmessage. (This should not be needed as the value returned
in case of an error contains the error message.)

try is implemented using tryCatch; for
programming, instead of try(expr, silent = TRUE), something like
tryCatch(expr, error = function(e) e) (or other simple
error handler functions) may be more efficient and flexible.

It may be useful to set the default for outFile to
stdout(), i.e.,

  options(try.outFile = stdout()) 

instead of the default stderr(),
notably when try() is used inside a Sweave code
chunk and the error message should appear in the resulting document.

See Also

options for setting error handlers and suppressing the
printing of error messages;
geterrmessage for retrieving the last error message.
The underlying tryCatch provides more flexible means of
catching and handling errors.

assertCondition in package tools is related and
useful for testing.

Examples

Run this code

# NOT RUN {
## this example will not work correctly in example(try), but
## it does work correctly if pasted in
options(show.error.messages = FALSE)
try(log("a"))
print(.Last.value)
options(show.error.messages = TRUE)

## alternatively,
print(try(log("a"), TRUE))

## run a simulation, keep only the results that worked.
set.seed(123)
x <- stats::rnorm(50)
doit <- function(x)
{
    x <- sample(x, replace = TRUE)
    if(length(unique(x)) > 30) mean(x)
    else stop("too few unique points")
}
## alternative 1
res <- lapply(1:100, function(i) try(doit(x), TRUE))
## alternative 2
# }
# NOT RUN {
res <- vector("list", 100)
for(i in 1:100) res[[i]] <- try(doit(x), TRUE)
# }
# NOT RUN {
unlist(res[sapply(res, function(x) !inherits(x, "try-error"))])
# }

Run the code above in your browser using DataCamp Workspace

[This article was first published on R, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)


Want to share your content on R-bloggers? click here if you have a blog, or here if you don’t.

In a previous post, we looked at error handling in R with the tryCatch() function and how this could be used to write Java style try-catch-finally blocks. This time we’ll look at what can be done with the try() function and how we can easily process warning and error messages to take appropriate action when something goes wrong.

The try() function is really just a simplified interface to tryCatch(). To see how try() calls tryCatch() you can examine the guts of the try() function by typing try without parens at the R prompt but you may not like what you see. For those of us outside the R core development team, this is not a good place to start. The documentation seen with ?try is much better and has a useful example showing how try() can be used to generate simulated results, ignoring those that generated errors. But this documentation doesn’t address the kind of error handling one needs for “application logic” where different actions are taken depending on the kind of error issued.

For this post, we will more generally explore how try() can be used and how warning and error messages can be processed using geterrmessage() and grepl(). The important things to remember about these functions are:

  • try() only ignores warnings, not errors
  • options(warn = 2) turns warnings into errors
  • geterrmessage() returns the character string associated with the last error
  • grepl(pattern, string) returns TRUE if pattern is found within string, FALSE otherwise

With just these functions we have everything we need to write very simple constructs that can evaluate a function and handle both errors and warnings. The test script at the end of this post demonstrates how messages and errors can be generated within a function and then trapped and processed by a calling function, potentially generating new errors that could be passed upstream.

Ideally, a function like myFunc() would validate incoming parameters with e.g. is.numeric() before attempting to use them but with more complicated objects like data frames this is not always possible. Also note that pattern matching on error strings depends on the stability of the error string so be careful! Nevertheless, this approach allows for quick development and expansion of application logic that can gracefully handle errors.

Just copy and paste the script at the end, make it executable and try it out with the these shell commands:

$ chmod +x try.R
$ ./try.R 2
$ ./try.R 1
$ ./try.R 0
$ ./try.R a
$ ./try.R
$ ./try.R warning
$ ./try.R error-A
$ ./try.R error-B

And here is the script.

#!/usr/bin/env Rscript
# try.R -- experiments with try

# Get any arguments
arguments <- commandArgs(trailingOnly = TRUE)
a <- arguments[1]

# Define a function that can issue custom warnings and errors
# Use '.call = FALSE' to remove the function call from the message
myFunc <- function(a) {
  if (a == 'warning') {
    return_value <- 'warning return'
    warning("custom warning message", call. = FALSE)
  } else if (a == 'error-A') {
    return_value <- 'error return'
    stop("custom error message A", call. = FALSE)
  } else if (a == 'error-B') {
    return_value <- 'error return'
    stop("custom error message B", call. = FALSE)
  } else {
    return_value = log(a)
  }
  return(return_value)
}

# Turn warnings into errors so they can be trapped
options(warn = 2)

# Put one or more lines of R code inside {...} to be
# evaluated together.
result <- try({
    myFunc(a)
  }, silent = TRUE)

# Process any error messages
if (class(result) == "try-error") {
  # Ignore warnings while processing errors
  options(warn = -1)

  # If this script were a function, warning() and stop()
  # could be called to pass errors upstream
  msg <- geterrmessage()
  if (grepl("missing value", msg)) {
    result <- paste("USER ERROR: Did you supply an argument?")
  } else if (grepl("non-numeric argument",msg)) {
    # Oops, forgot to convert argument to numeric
    a <- as.numeric(a)
    if (is.na(a)) {
      result <- "Argument is non-numeric"
    } else {
      result <- myFunc(a)
    }
  } else if (grepl("custom warning message", msg)) {
    result <- "Took action for warning message"
  } else if (grepl("custom error message A", msg)) {
    result <- "Took action for error message A"
  } else if (grepl("custom error message B", msg)) {
    result <- "Could call stop() to pass an error upstream."
  }

  # Restore default warning reporting
  options(warn = 0)
}

print(result)

A previous version of this article originally appeared at WorkingwithData.

There are many situations in which you should handle errors properly or
ensure they do not happen in the first place when using R.
This is especially true if you automate long-running tasks or when writing
code other people use (like a package).

Signalling errors

R allows you to manually signal that an error occurred by calling stop.

testX <- function(x) {
    if (is.character(x)) {
        stop("x must not be of type character")
    }
}
testX("abc")
## Error in testX("abc"): x must not be of type character

Unlike in other languages like Python there are no different types of errors
in R; there is nothing like TypeError or ValueError.
All errors you signal with stop are the same and you should be specific
(but concise) with the error message.

However, sometimes an error might be too much for what you are trying to do,
so you might want to use the less severe levels of signalling: warning
and message.

squareX <- function(x) {
    if (is.character(x)) {
        warning("Converting x to numeric")
        x <- as.numeric(x)
    } else {
        # the type checking done here is of course very incomplete
        message("x appears to be numeric")
    }
    x ^ 2 
}
squareX("4")
## Warning in squareX("4"): Converting x to numeric
## [1] 16
squareX(5)
## x appears to be numeric
## [1] 25

In both cases execution of the code will continue, but the warning or
message is displayed to the user.
You should probably ensure that you do not use warning and message
excessively, otherwise users of your code might be tempted to call
suppressWarnings or suppressMessages, kind of defeating the purpose of
your warnings and messages.

stopifnot for assertions

The pattern in our very first example can be expressed more elegantly using
stopifnot.
This function does exactly what the name suggests: it stops if a condition is
not met.

testX <- function(x) {
    stopifnot(
        x > 0, 
        is.numeric(x)
    )
}
testX("abc")
## Error in testX("abc"): is.numeric(x) is not TRUE
testX(-10)
## Error in testX(-10): x > 0 is not TRUE

You can provide many conditions to stopifnot and code execution will only
continue if all of these are met.
Typically, stopifnot is more suitable for using at the beginning of your
function, while calling stop is better suited for using deeper within your
function.
The main issue here is that you can control the message outputted by stop,
but not the message stopifnot generates.
As stopifnot clearly refers to the variable that was checked it can be
confusing if your users see errors referring to internal variables of your
function.

Ignoring errors with try

Sometimes you want to keep going when an error occurs.
For example when you run a job that processes a bunch of data sets and it takes
a while to finish, you might want to ensure
that things keep going when the processing of a single data set fails.
If you have a job that you expect will take a few days to finish
and you then have look at it after a while and realise it failed after
a few hours, that is not exactly a nice feeling.
Or so I have heard…

This is where try is useful.
If whatever is wrapped in try throws an error, try will return an object
of class try-error and your code will keep on going afterwards.

err <- try(mean())
## Error in mean.default() : argument "x" is missing, with no default
err
## [1] "Error in mean.default() : argument "x" is missing, with no defaultn"
## attr(,"class")
## [1] "try-error"
## attr(,"condition")
## <simpleError in mean.default(): argument "x" is missing, with no default>

If you use something like lapply to run your job it will be very easy to
filter out the items where the processing failed by omitting all
items which have the try-error class.
While try is very simple, used in the right places it can make your life a
lot easier.

Being more fine grained: tryCatch and withCallingHandlers

Sometimes you need a bit more granularity in handling conditions.
For example you might want to also be able to handle warnings and log
messages somewhere.
This is where tryCatch and withCallingHandlers come in.
Both are very similar, but behave slightly differently.

Let’s first look at tryCatch:

my_function <- function() {
    mean(letters) 
    x <- mean(1:10)
    x
}

tryCatch(
    expr = { 
        my_function()
    }, 
    error = function(e) {
        print("An error happened.")
    },
    warning = function(e) {
        print("Something does not look right.")
    },
    message = function(e) {
        print("You should make sure results are correct.")
    }
)
## [1] "Something does not look right."

Our not very useful function does generate a warning in its first statement.1
This warning is handled by tryCatch and the execution of our function is
halted an that point.

Now, we will do the same with withCallingHandlers:

withCallingHandlers(
    expr = { 
        my_function()
    }, 
    error = function(e) {
        print("An error happened.")
    },
    warning = function(e) {
        print("something does not look right")
    },
    message = function(e) {
        print("You should make sure results are correct.")
    }
)
## [1] "something does not look right"
## Warning in mean.default(letters): argument is not numeric or logical: returning
## NA
## [1] 5.5

We spot to crucial differences:

  1. Our function keeps going after the warning was thrown. Whatever we specify
    in warning is executed, but our function keeps going.
  2. The condition is not consumed by withCallingHandlers, we do see the
    warning that was raised.

This probably makes you think that if the condition is raised again by
withCallingHandlers, what happens when there is an error?

Let’s first try this with tryCatch:

my_new_function <- function() {
    stop("oh no :(")
    print("this line will never be printed")
}

tryCatch(
    expr = { 
        my_new_function()
    }, 
    error = function(e) {
        print("An error happened.")
    },
    warning = function(e) {
        print("something does not look right")
    },
    message = function(e) {
        print("You should make sure results are correct.")
    }
)
## [1] "An error happened."

Everything acts as expected. We do not get an error, but it is handled and
consumed.
So, something gets wrong and it is handled, pretty much what error handling
sounds like it should do.

Now, let’s try again with withCallingHandlers:

withCallingHandlers(
    expr = { 
        my_new_function()
    }, 
    error = function(e) {
        print("An error happened.")
    },
    warning = function(e) {
        print("something does not look right")
    },
    message = function(e) {
        print("You should make sure results are correct.")
    }
)
## [1] "An error happened."
## Error in my_new_function(): oh no :(

And indeed we do error out.

So, you might want to use withCallingHandlers only for dealing with
messages and warnings, but probably not with errors, for which
tryCatch is more suitable.

And as you might have guessed by now, you can combine both to gracefully
handle all conditions.

tryCatch({
    withCallingHandlers(
        expr = {
            my_new_function()
        },
        error = function(e) {
            print("An error occured")
        }
    )},
    error = function(e) {
        print("Failing with style")
    }
)
## [1] "An error occured"
## [1] "Failing with style"

At this point the code becomes a bit convoluted, but it does the job.

Defining your own conditions

R allows you to define your own conditions if you really want to do some
very specific error handling.
It is kind of straightforward by creating an object that has the
class condition.

my_condition <- function(msg = "my condition") {
    structure(
        class = c("my_condition", "condition"),
        list(message = msg)
    )
}

Of course you are free to add whatever more information you would like to
include in your class, having a look at ?sys.call might be worthwhile
for example.

You can use your shiny new condition like the default ones as well.

withCallingHandlers(
    expr = {
        signalCondition(my_condition())
        print("the above condition was handled")
    },
    my_condition = function(e) {
        print("handling condition...")
    }
)
## [1] "handling condition..."
## [1] "the above condition was handled"

But I suppose it is kind of rare that there is need to build your own
conditions with R.

Packages for condition handling and assertions

An alternative to stopifnot is the assertthat package.

foo <- function(x) {
    assertthat::assert_that(x == 1, msg = "x must always be 1")
    "yay"
} 
foo(1)
## [1] "yay"
foo(2)
## Error: x must always be 1

The package provides a bunch of helpers to check various conditions.

There is also the
tryCatchLog
package providing alternatives to try and tryCatch.

tryCatchLog::tryLog(stop("error :("))
## ERROR [2022-04-14 12:36:03] error :(
## 
## Compact call stack:
##   1 local({
## 
## Full call stack:
##   1 local({
##         if (length(a <- commandArgs(TRUE)) != 2) 
##             stop("T
##   2 eval.parent(substitute(eval(quote(expr), envir)))
##   3 eval(expr, p)
##   4 eval(expr, p)
##   5 eval(quote({
##         if (length(a <- commandArgs(TRUE)) != 2) stop("The numb
##   6 eval(quote({
##         if (length(a <- commandArgs(TRUE)) != 2) stop("The numb
##   7 do.call(f, x[[2]], envir = globalenv())
##   8 (function (input, output, to_md = file_ext(output) != "html", quiet = TRUE)
##   9 rmarkdown::render(input, "blogdown::html_page", output_file = output, envir
##   10 knitr::knit(knit_input, knit_output, envir = envir, quiet = quiet)
##   11 process_file(text, output)
##   12 withCallingHandlers(if (tangle) process_tangle(group) else process_group(gr
##   13 process_group(group)
##   14 process_group.block(group)
##   15 call_block(x)
##   16 block_exec(params)
##   17 eng_r(options)
##   18 in_dir(input_dir(), evaluate(code, envir = env, new_device = FALSE, keep_wa
##   19 evaluate(code, envir = env, new_device = FALSE, keep_warning = !isFALSE(opt
##   20 evaluate::evaluate(...)
##   21 evaluate_call(expr, parsed$src[[i]], envir = envir, enclos = enclos, debug 
##   22 timing_fn(handle(ev <- withCallingHandlers(withVisible(eval(expr, envir, en
##   23 handle(ev <- withCallingHandlers(withVisible(eval(expr, envir, enclos)), wa
##   24 try(f, silent = TRUE)
##   25 tryCatch(expr, error = function(e) {
##         call <- conditionCall(e)
##        
##   26 tryCatchList(expr, classes, parentenv, handlers)
##   27 tryCatchOne(expr, names, parentenv, handlers[[1]])
##   28 doTryCatch(return(expr), name, parentenv, handler)
##   29 withCallingHandlers(withVisible(eval(expr, envir, enclos)), warning = wHand
##   30 withVisible(eval(expr, envir, enclos))
##   31 eval(expr, envir, enclos)
##   32 eval(expr, envir, enclos)
##   33 tryCatchLog::tryLog(stop("error :("))
##   34 tryCatchLog(expr = expr, execution.context.msg = execution.context.msg, wri
##   35 tryCatch(withCallingHandlers(expr, condition = cond.handler), ..., finally 
##   36 tryCatchList(expr, classes, parentenv, handlers)
##   37 tryCatchOne(expr, names, parentenv, handlers[[1]])
##   38 doTryCatch(return(expr), name, parentenv, handler)
##   39 withCallingHandlers(expr, condition = cond.handler)
##   40 stop("error :(")
##   41 .handleSimpleError(function (c) 
##     {
##         if (inherits(c, "condition") &

It does not only provide detailed call stacks when something goes wrong,
it also makes it straightforward to log conditions to a file.

So, what should you use?

To be honest, in the vast majority of cases simply using try and
stopifnot is enough to make sure nothing goes terribly wrong.

When things are more complicated the tryCatchLog package is probably
worth a look, because when there is that much need for elaborate handling
of conditions the features it adds are probably very welcome.

Исключение языка R или обработка ошибок


Вопрос 1. В процессе использования языка R (пакет RCurl) для сканирования веб-страниц у некоторых страниц часто истекает время ожидания или страницы не существуют, что приводит к завершению работы программы из-за ненормального прерывания, а затем программа, которая автоматически захватывает данные в пакетах, прерывается. Требуется ручное вмешательство для повторного запуска программы или перезапуска службы, что приводит к увеличению затрат на обслуживание.

Вопрос 2: При использовании языка R для обработки данных часто необходимо писать пакетные программы для реализации автоматической обработки программы, но могут возникать некоторые непредвиденные ошибки, которые приведут к прерыванию процесса автоматизации. В это время требуется ручное вмешательство и добавляются ненужные трудозатраты.

Мы надеемся, что во время выполнения программы, если будут обнаружены некоторые предсказуемые ошибки, они могут быть обработаны автоматически, игнорировать эти исключения и продолжить выполнение следующего кода, тогда вы можете использовать функции try, tryCatch, withCallingHandlers для обработки исключений и позволить программе продолжить работу. выполненный.

1. В R есть три функциональных инструмента, которые могут решить проблему обработки условных исключений (включая ошибки):

  • try()В случае возникновения ошибки используйте эту функцию, чтобы пропустить ошибку и продолжить выполнение программы.
  • tryCatch()Задайте условия управления, перехватите исключения, а затем используйте соответствующие функции для обработки исключений и ошибок.
  • withCallingHandlers()Это вариант tryCatch (), но контекст операции другой, он используется редко, но очень полезен.

2. Подробное объяснение и примеры параметров функции.

  • try()

    • Обработка исключений в языке R аналогична Java, с использованием оператора try () для перехвата исключений, но нет соответствующего оператора catch ().
    • После перехвата исключения функцией try () захваченный объект анализируется.
    • Первый параметр функции try () — это вызываемый метод, а второй параметр — отображать ли сообщение об исключении, напримерtry(…, silent=TRUE)

    Если при выполнении выражения генерируется сообщение об ошибке, функция try () вернет объект класса’ry-error ‘. Если параметр silent = TRUE, сообщение об ошибке будет скрыто, а silent = FALSE, сообщение об ошибке будет отображаться на экране. В этом случае, если тип ошибки’ry-error ‘находится в классе переменной x.inv, мы вызываем следующий оператор, чтобы завершить выполнение текущего цикла и перейти к следующему циклу, в противном случае мы добавляем x.inv Значение выражения обратное. (Например, см. Следующий код)

      ###question1:###
      ### Произошла ошибка в процессе решения обратной матрицы! ! ! Как пропустить ошибку! ! ! ###
      set.seed(1)
      count <- 1
      inverses <- vector(mode = "list", 100)
      repeat {
        x <- matrix(sample(0:2, 4, replace = T), 2, 2)
        inverses[[count]] <- solve(x)
        count <- count + 1
        if (count > 100) break
      }
      ################
    
      ###answer1:#####
      count <- 0
      inverses <- vector(mode = "list", 100)  
      repeat {  
          if (count == 100) break  
          count <- count + 1  
          x <- matrix(sample(0:2, 4, replace = T), 2, 2)  
          x.inv <- try(solve(x), silent=TRUE)  
          if ('try-error' %in% class(x.inv)) {
              next
          } else{ 
              inverses[[count]] <- x.inv  
          }
      } 
      inverses
      #inverses[!is.null(inverses)]
      inverses[!(inverses=='NULL')]
      ###############

      try()Разрешить продолжение выполнения кода после возникновения ошибки. Например, как правило, если выполняемая вами функция вызывает ошибку, она немедленно завершается и не возвращает значения:

      f1 <- function(x) {
        log(x)
        10
      }
      f1("x")
      #> Error in log(x): non-numeric argument to mathematical function

    Однако, если вы поместите оператор ошибки в try (), сообщение об ошибке будет напечатано, но программа продолжит выполнение:

      f2 <- function(x) {
        try(log(x))
        10
      }
      f2("a")
      #> Error in log(x) : non-numeric argument to mathematical function
      #> [1] 10

    Мы можем использоватьtry(…, silent=TRUE)Функция для скрытия информации об ошибках и исключениях.

    Если есть ошибки в большом разделе кода и вы хотите их игнорировать, вы можете использоватьtry(), Но большую часть кода нужно разместить{ }в:

        # По умолчанию silent = FALSE, отображать сообщение об ошибке
      try({
        a <- 1
        b <- "x"
        a + b
      })
    
         # Скрыть сообщения об ошибках
      try({
        a <- 1
        b <- "x"
        a + b
      } ,  silent = TRUE)

    Вы можете захватитьtry()Если программа выполняется успешно, вернуть результат расчета; если программа работает безуспешно, можно пройтиclass()Функция возвращает, тип ошибки — «ошибка».

      success <- try(1 + 2)
      failure <- try("a" + "b")
      class(success)
      #> [1] "numeric"
      class(failure)
      #> [1] "try-error"
      ('try-error' %in% class(success))
      #> [1] FALSE
      ('try-error' %in% class(failure))
      #> [1] TRUE

    Использовать в спискеtry()Эта функция очень полезна и может эффективно избежать ошибок, вызванных невозможностью вычислить отдельные элементы.

      elements <- list(1:10, c(-1, 10), c(T, F), letters)
      results <- lapply(elements, log)
      #> Warning in FUN(X[[i]], ...): NaNs produced
      #> Error in FUN(X[[i]], ...): non-numeric argument to mathematical function
      results <- lapply(elements, function(x) try(log(x)))
      #> Warning in log(x): NaNs produced

    В R нет функции, которая может идентифицировать тип (класс) ошибки — «try-error». Мы можем настроить функцию, а затем объединить функцию sapply, чтобы легко извлечь тип ошибки, местоположение ошибки, а также значение ошибки и правильность. ценность.

      is.error <- function(x) inherits(x, "try-error")
      succeeded <- !sapply(results, is.error)
    
      # look at successful results
      str(results[succeeded])
      #> List of 3
      #>  $ : num [1:10] 0 0.693 1.099 1.386 1.609 ...
      #>  $ : num [1:2] NaN 2.3
      #>  $ : num [1:2] 0 -Inf
    
      # look at inputs that failed
      str(elements[!succeeded])
      #> List of 1
      #>  $ : chr [1:26] "a" "b" "c" "d" ...

      try()Очень практичное использование выглядит следующим образом:

      default <- NULL
      try(default <- read.csv("possibly-bad-input.csv"), silent = TRUE)
  • tryCatch()

    НижеtryCatch()Стандартный синтаксис функций:

      result = tryCatch({
                 # Нормальная логика
          expr
      }, warning = function(w) {
                 # Появляется логика обработки предупреждения
          warning-handler-code
      }, error = function(e) {
                 # Логика обработки ошибок
          error-handler-code
      }, finally = {
                 #Code модуль, который будет выполняться независимо от того, что он ненормален,
                 # Обычно используется для обработки операций очистки, таких как закрытие ресурсов соединения.
          cleanup-code
      }

    Два небольших практических примера,code

      #code1:
      get.msg <- function(path)
      {
        con <- file(path, open = "rt", encoding = "latin1")
        text <- readLines(con)
        msg <- tryCatch({
          text[seq(which(text == "")[1] + 1, length(text), 1)]
        }, error = function(e) {
          ""
        })
        close(con)
        return(paste(msg, collapse = "n"))
      }
    
      #code2:
      library(RMySQL)
      result = tryCatch({
                 # Получить подключение для передачи данных
          connect <- dbConnect(MySQL(), dbname="db_olap_web", username="root", password="")
                 # Обработка другой логики
          #……
      }, warning = function(w) {
                 # Здесь я кратко разберусь
                 # То есть выводить на консоль
          print(w)
      }, error = function(e) {
                 # Вот я кратко разберусь
                 # То есть выводить на консоль
          print(e)
      }, finally = {
                 # Закрыть соединение с базой данных
          dbDisconnect(connect)
      }

    ИспользоватьtryCatch()Функция в соответствии с полученным сигналом состояния возвращает соответствующий результат обработки встроенной функции, ошибку, предупреждение, сообщение и т. Д.

      show_condition <- function(code) {
        tryCatch(code,
          error = function(c) "error",
          warning = function(c) "warning",
          message = function(c) "message"
        )
      }
      show_condition(stop("!"))
      #> [1] "error"
      show_condition(warning("?!"))
      #> [1] "warning"
      show_condition(message("?"))
      #> [1] "message"
    
      # If no condition is captured, tryCatch returns the 
      # value of the input
      show_condition(10)
      #> [1] 10

    Мы можем использовать функцию tryCatch (), чтобы реализовать функцию функции try (). Необходимо использовать conditionMessage () для извлечения сообщения, связанного с исходной ошибкой.

      try2 <- function(code, silent = FALSE) {
        tryCatch(code, error = function(c) {
          msg <- conditionMessage(c)
          if (!silent) message(c)
          invisible(structure(msg, class = "try-error"))
        })
      }
    
      try2(1)
      #> [1] 1
      try2(stop("Hi"))
      try2(stop("Hi"), silent = TRUE)

    Когда сигнал возвращаемого значения ошибки имеет значение по умолчанию, но мы хотим видеть более подробную информацию об ошибке, нам нужно инкапсулировать процесс функции tryCatch () самостоятельно, изменить объект информации об ошибке, чтобы сохранить больше ошибок Информация. Следующий пример — инкапсулировать ошибку функции read.csv () и добавить путь к сообщению об ошибке! ! !

      read.csv2 <- function(file, ...) {
        tryCatch(read.csv(file, ...), error = function(c) {
          c$message <- paste0(c$message, " (in ", file, ")")
          stop(c)
        })
      }
      read.csv("code/dummy.csv")
      #> Error in file(file, "rt"): cannot open the connection
      read.csv2("code/dummy.csv")
      #> Error in file(file, "rt"): cannot open the connection (in code/dummy.csv)

    При использовании tryCatch () для перехвата исключений и прерывания программного кода необходимо обращать внимание на ситуацию, которая может вызвать бесконечный цикл. (Если вы не убьете процесс программы R !!!)

      # Don't let the user interrupt the code
      i <- 1
      while(i < 3) {
        tryCatch({
          Sys.sleep(0.5)
          message("Try to escape")
        }, interrupt = function(x) {
          message("Try again!")
          i <<- i + 1
        })
      }

    TryCatch () также имеет функциональный модуль: finally = {cleanup-code}, который указывает блок кода (код очистки) (не функцию) и запускает этот блок кода независимо от того, успешно или нет исходное выражение. Это очень полезно для очистки программ (например, удаления файлов, закрытия соединений). Эта функция эквивалентна использованию on.exit (), но ее можно инкапсулировать в меньший блок кода.

  • withCallingHandlers()

    Другой метод, похожий на tryCatch (), — withCallingHandlers (). Между их функциями есть два основных различия:

    • Возвращаемое значение обработчика tryCatch () возвращается функцией tryCatch (), а возвращаемое значение withCallingHandlers () игнорируется обработчиком.

        f <- function() stop("!")
        tryCatch(f(), error = function(e) 1)
        #> [1] 1
        withCallingHandlers(f(), error = function(e) 1)
        #> Error in f(): !
    • Вызывая sys.calls () для просмотра соответствующего промежуточного процесса, его работа эквивалентна использованию traceback (), как показано ниже, он перечисляет все вызовы, ведущие к текущей функции.

        f <- function() g()
        g <- function() h()
        h <- function() stop("!")
      
        tryCatch(f(), error = function(e) print(sys.calls()))
        # [[1]] tryCatch(f(), error = function(e) print(sys.calls()))
        # [[2]] tryCatchList(expr, classes, parentenv, handlers)
        # [[3]] tryCatchOne(expr, names, parentenv, handlers[[1L]])
        # [[4]] value[[3L]](cond)
      
        withCallingHandlers(f(), error = function(e) print(sys.calls()))
        # [[1]] withCallingHandlers(f(), 
        #    error = function(e) print(sys.calls()))
        # [[2]] f()
        # [[3]] g()
        # [[4]] h()
        # [[5]] stop("!")
        # [[6]] .handleSimpleError(
        #    function (e) print(sys.calls()), "!", quote(h()))
        # [[7]] h(simpleError(msg, call))

    Ниже приводится примерcode

      message2error <- function(code) {
        withCallingHandlers(code, message = function(e) stop(e))
      }
    
      f <- function() g()
      g <- function() message("Hi!")
      g()
      # Error in message("Hi!"): Hi!
      message2error(g())
      traceback()
      # 10: stop(e) at #2
      # 9: (function (e) stop(e))(list(message = "Hi!n", 
      #      call = message("Hi!")))
      # 8: signalCondition(cond)
      # 7: doWithOneRestart(return(expr), restart)
      # 6: withOneRestart(expr, restarts[[1L]])
      # 5: withRestarts()
      # 4: message("Hi!") at #1
      # 3: g()
      # 2: withCallingHandlers(code, message = function(e) stop(e)) 
      #      at #2
      # 1: message2error(g())

    Эти тонкие различия используются редко, за исключением случаев, когда вы пытаетесь зафиксировать, что пошло не так, и передать это другой функции. В большинстве случаев вам не нужно использовать withCallingHandlers ().

Исчерпывающий пример

    #!/usr/bin/env Rscript
    # tryCatch.r -- experiments with tryCatch
    
    # Get any arguments
    arguments <- commandArgs(trailingOnly=TRUE)
    a <- arguments[1]
    
    # Define a division function that can issue warnings and errors
    myDivide <- function(d, a) {
      if (a == 'warning') {
        return_value <- 'myDivide warning result'
        warning("myDivide warning message")
      } else if (a == 'error') {
        return_value <- 'myDivide error result'
        stop("myDivide error message")
      } else {
        return_value = d / as.numeric(a)
      }
      return(return_value)
    }
    
    # Evalute the desired series of expressions inside of tryCatch
    result <- tryCatch({
      
      b <- 2
      c <- b^2
      d <- c+2
      if (a == 'suppress-warnings') {
        e <- suppressWarnings(myDivide(d,a))
      } else {
        e <- myDivide(d,a) # 6/a
      }
      f <- e + 100
      
    }, warning = function(war) {
      
      # warning handler picks up where error was generated
      print(paste("MY_WARNING:  ",war))
      b <- "changing 'b' inside the warning handler has no effect"
      e <- myDivide(d,0.1) # =60
      f <- e + 100
      return(f)
      
    }, error = function(err) {
      
      # warning handler picks up where error was generated
      print(paste("MY_ERROR:  ",err))
      b <- "changing 'b' inside the error handler has no effect"
      e <- myDivide(d,0.01) # =600
      f <- e + 100
      return(f)
      
    }, finally = {
      
      print(paste("a =",a))
      print(paste("b =",b))
      print(paste("c =",c))
      print(paste("d =",d))
      # NOTE:  Finally is evaluated in the context of of the inital
      # NOTE:  tryCatch block and 'e' will not exist if a warning
      # NOTE:  or error occurred.
      #print(paste("e =",e))
      
    }) # END tryCatch
    
    print(paste("result =",result))

Ссылка

  • Debugging, condition handling, and defensive programming

  • Using R — Basic error Handing with tryCatch()

  • Язык R — три примера обработки аномальных значений или ошибок

  • Как использовать tryCatch для обработки исключений при выполнении языка R

  • Язык R использует tryCatch для простой обработки ошибок

  • how-to-skip-an-error-in-a-loop

  • skip-to-next-value-of-loop-upon-error-in-r-trycatch

  • How can I check whether a function call results in a warning?

  • Exception handling in R

  • tolstoy.newcastle.edu.au

  • stackoverflow.com/search

  • github.com/search

When you are writing big programs, sometimes something goes wrong with your R code. What do you do if the program stops unexpectedly? What tools do you have to address the problem? This is where the tryCatch() function will help you. Debugging is the art and science of fixing unexpected problems in your code.

Error Handling is a process in which we deal with an unwanted or anomalous error that may cause unnatural termination of the program during its execution.

The basic functions that one can use for error handling in R are the following.

  1. stop(…): The stop() function halts the evaluation of the current statement and generates a message argument. The control is returned to the top-level.
  2. waiting(…): Its evaluation depends on the value of the error option warn. If the value of the warning is negative, then it is ignored. In case the value is 0 (zero), they are stored and printed only after the top-level function completes its execution. If the value is 1 (one), then it is printed as soon as it has been encountered, while if the value is 2 (two), then immediately, the generated warning is converted into an error.
  3. tryCatch(…): The tryCatch() function helps evaluate the code and assign the exceptions.

In this article, we will see how the tryCatch() function works.

The tryCatch() function in R evaluates an expression with the possibility to catch exceptions. The class of the exception thrown by a standard stop() call is try-error. The tryCatch() function allows the users to handle errors. With it, you can do things like: if(error), then(do this).

In tryCatch(), there are two ‘conditions’ that can be handled: ‘warnings’ and ‘errors’. The important thing to understand when writing each block of code is the state of execution and the scope.

Syntax

result = tryCatch({
 expression
}, warning = function(w) {
 warning-handler-code
}, error = function(e) {
 error-handler-code
}, finally = {
 cleanup-code
}

Parameters

expression: The expression to be evaluated.
…: A catch list of named expressions. The expression with the same name as the class of the Exception thrown when evaluating an expression.
finally: An expression that is guaranteed to be called even if the expression generates an exception.
envir: The environment in which the caught expression is to be evaluated.

Example

Let’s try to find the log of character value in R and analyze the output.

Output

Error in log("ABC") : non-numeric argument to mathematical function
Execution halted

We can see that we got an error saying: non-numeric argument to a mathematical function execution halted.

Now maybe you would want something to happen when such an error happens. You can handle this kind of error using the tryCatch() function.

tryCatch(log("ABC"),
 error = function(e)
 print("You can't calculate the log of a character"))

Output

[1] "You can't calculate the log of a character"

The tryCatch() function returns the value associated with executing the expression unless there’s an error or a warning.

In some cases, the specific return values (see a return(NA) above) can be specified by supplying a respective handler function (see arguments error and warning in ?tryCatch).

Second Example of tryCatch() in R

Write the following code inside the R file.

arg <- 0

div <- function(num, a) {
 if (a == "warning") {
 val <- "It is a warning"
 warning("warning message")
 }
 else if (a == "error") {
 val <- "It is an error"
 stop("error!!")
 }
 else {
 val <- num / as.numeric(a)
 }
 return(val)
}

vl <- tryCatch({

 b <- 1
 e <- div(b, arg)

},
warning = function(warn) {
 print(paste("MY WARNING: ", warn))
},
error = function(err) {
 print(paste("MY ERROR: ", err))
},
finally = function(f) {
 print(paste("e: ", e))
})
print(paste("Output: ", vl))

Output

In this example, you can see that if we are dividing any value with 0, then it will return an Infinity or Inf in our case.

That is it for tryCatch() function in R language.

Krunal Lathiya Author

Krunal Lathiya is a Software Engineer with over eight years of experience. He has developed a strong foundation in computer science principles and a passion for problem-solving. In addition, Krunal has excellent knowledge of Data Science and Machine Learning, and he is an expert in R Language.

Introduction

The condition system provides a paired set of tools that allow the author of a function to indicate that something unusual is happening, and the user of that function to deal with it. The function author signals conditions with functions like stop() (for errors), warning() (for warnings), and message() (for messages), then the function user can handle them with functions like tryCatch() and withCallingHandlers(). Understanding the condition system is important because you’ll often need to play both roles: signalling conditions from the functions you create, and handle conditions signalled by the functions you call.

R offers a very powerful condition system based on ideas from Common Lisp. Like R’s approach to object-oriented programming, it is rather different to currently popular programming languages so it is easy to misunderstand, and there has been relatively little written about how to use it effectively. Historically, this has meant that few people (myself included) have taken full advantage of its power. The goal of this chapter is to remedy that situation. Here you will learn about the big ideas of R’s condition system, as well as learning a bunch of practical tools that will make your code stronger.

I found two resources particularly useful when writing this chapter. You may also want to read them if you want to learn more about the inspirations and motivations for the system:

  • A prototype of a condition system for R by Robert Gentleman
    and Luke Tierney. This describes an early version of R’s condition system.
    While the implementation has changed somewhat since this document was
    written, it provides a good overview of how the pieces fit together, and
    some motivation for its design.

  • Beyond exception handling: conditions and restarts
    by Peter Seibel. This describes exception handling in Lisp, which happens
    to be very similar to R’s approach. It provides useful motivation and
    more sophisticated examples. I have provided an R translation of the
    chapter at http://adv-r.had.co.nz/beyond-exception-handling.html.

I also found it helpful to work through the underlying C code that implements these ideas. If you’re interested in understanding how it all works, you might find my notes to be useful.

Quiz

Want to skip this chapter? Go for it, if you can answer the questions below. Find the answers at the end of the chapter in Section 8.7.

  1. What are the three most important types of condition?

  2. What function do you use to ignore errors in block of code?

  3. What’s the main difference between tryCatch() and withCallingHandlers()?

  4. Why might you want to create a custom error object?

Outline

  • Section 8.2 introduces the basic tools for
    signalling conditions, and discusses when it is appropriate to use each type.

  • Section 8.3 teaches you about the simplest tools for
    handling conditions: functions like try() and supressMessages() that
    swallow conditions and prevent them from getting to the top level.

  • Section 8.4 introduces the condition object, and
    the two fundamental tools of condition handling: tryCatch() for error
    conditions, and withCallingHandlers() for everything else.

  • Section 8.5 shows you how to extend the built-in
    condition objects to store useful data that condition handlers can use to
    make more informed decisions.

  • Section 8.6 closes out the chapter with a grab bag
    of practical applications based on the low-level tools found in earlier
    sections.

Prerequisites

As well as base R functions, this chapter uses condition signalling and handling functions from rlang.

Signalling conditions

There are three conditions that you can signal in code: errors, warnings, and messages.

  • Errors are the most severe; they indicate that there is no way for a function
    to continue and execution must stop.

  • Warnings fall somewhat in between errors and message, and typically indicate
    that something has gone wrong but the function has been able to at least
    partially recover.

  • Messages are the mildest; they are way of informing users that some action
    has been performed on their behalf.

There is a final condition that can only be generated interactively: an interrupt, which indicates that the user has interrupted execution by pressing Escape, Ctrl + Break, or Ctrl + C (depending on the platform).

Conditions are usually displayed prominently, in a bold font or coloured red, depending on the R interface. You can tell them apart because errors always start with “Error”, warnings with “Warning” or “Warning message”, and messages with nothing.

stop("This is what an error looks like")
#> Error in eval(expr, envir, enclos): This is what an error looks like

warning("This is what a warning looks like")
#> Warning: This is what a warning looks like

message("This is what a message looks like")
#> This is what a message looks like

The following three sections describe errors, warnings, and messages in more detail.

Errors

In base R, errors are signalled, or thrown, by stop():

f <- function() g()
g <- function() h()
h <- function() stop("This is an error!")

f()
#> Error in h(): This is an error!

By default, the error message includes the call, but this is typically not useful (and recapitulates information that you can easily get from traceback()), so I think it’s good practice to use call. = FALSE46:

h <- function() stop("This is an error!", call. = FALSE)
f()
#> Error: This is an error!

The rlang equivalent to stop(), rlang::abort(), does this automatically. We’ll use abort() throughout this chapter, but we won’t get to its most compelling feature, the ability to add additional metadata to the condition object, until we’re near the end of the chapter.

h <- function() abort("This is an error!")
f()
#> Error: This is an error!

(NB: stop() pastes together multiple inputs, while abort() does not. To create complex error messages with abort, I recommend using glue::glue(). This allows us to use other arguments to abort() for useful features that you’ll learn about in Section 8.5.)

The best error messages tell you what is wrong and point you in the right direction to fix the problem. Writing good error messages is hard because errors usually occur when the user has a flawed mental model of the function. As a developer, it’s hard to imagine how the user might be thinking incorrectly about your function, and thus it’s hard to write a message that will steer the user in the correct direction. That said, the tidyverse style guide discusses a few general principles that we have found useful: http://style.tidyverse.org/error-messages.html.

Warnings

Warnings, signalled by warning(), are weaker than errors: they signal that something has gone wrong, but the code has been able to recover and continue. Unlike errors, you can have multiple warnings from a single function call:

By default, warnings are cached and printed only when control returns to the top level:

fw()
#> 1
#> 2
#> 3
#> Warning messages:
#> 1: In f() : W1
#> 2: In f() : W2
#> 3: In f() : W3

You can control this behaviour with the warn option:

  • To make warnings appear immediately, set options(warn = 1).

  • To turn warnings into errors, set options(warn = 2). This is usually
    the easiest way to debug a warning, as once it’s an error you can
    use tools like traceback() to find the source.

  • Restore the default behaviour with options(warn = 0).

Like stop(), warning() also has a call argument. It is slightly more useful (since warnings are often more distant from their source), but I still generally suppress it with call. = FALSE. Like rlang::abort(), the rlang equivalent of warning(), rlang::warn(), also suppresses the call. by default.

Warnings occupy a somewhat challenging place between messages (“you should know about this”) and errors (“you must fix this!”), and it’s hard to give precise advice on when to use them. Generally, be restrained, as warnings are easy to miss if there’s a lot of other output, and you don’t want your function to recover too easily from clearly invalid input. In my opinion, base R tends to overuse warnings, and many warnings in base R would be better off as errors. For example, I think these warnings would be more helpful as errors:

formals(1)
#> Warning in formals(fun): argument is not a function
#> NULL

file.remove("this-file-doesn't-exist")
#> Warning in file.remove("this-file-doesn't-exist"): cannot remove file 'this-
#> file-doesn't-exist', reason 'No such file or directory'
#> [1] FALSE

lag(1:3, k = 1.5)
#> Warning in lag.default(1:3, k = 1.5): 'k' is not an integer
#> [1] 1 2 3
#> attr(,"tsp")
#> [1] -1  1  1

as.numeric(c("18", "30", "50+", "345,678"))
#> Warning: NAs introduced by coercion
#> [1] 18 30 NA NA

There are only a couple of cases where using a warning is clearly appropriate:

  • When you deprecate a function you want to allow older code to continue
    to work (so ignoring the warning is OK) but you want to encourage the user
    to switch to a new function.

  • When you are reasonably certain you can recover from a problem:
    If you were 100% certain that you could fix the problem, you wouldn’t need
    any message; if you were more uncertain that you could correctly fix the
    issue, you’d throw an error.

Otherwise use warnings with restraint, and carefully consider if an error would be more appropriate.

Messages

Messages, signalled by message(), are informational; use them to tell the user that you’ve done something on their behalf. Good messages are a balancing act: you want to provide just enough information so the user knows what’s going on, but not so much that they’re overwhelmed.

message()s are displayed immediately and do not have a call. argument:

Good places to use a message are:

  • When a default argument requires some non-trivial amount of computation
    and you want to tell the user what value was used. For example, ggplot2
    reports the number of bins used if you don’t supply a binwidth.

  • In functions that are called primarily for their side-effects which would
    otherwise be silent. For example, when writing files to disk, calling a web
    API, or writing to a database, it’s useful to provide regular status messages
    telling the user what’s happening.

  • When you’re about to start a long running process with no
    intermediate output. A progress bar (e.g. with
    progress) is better, but a message
    is a good place to start.

  • When writing a package, you sometimes want to display a message when
    your package is loaded (i.e. in .onAttach()); here you must use
    packageStartupMessage().

Generally any function that produces a message should have some way to suppress it, like a quiet = TRUE argument. It is possible to suppress all messages with suppressMessages(), as you’ll learn shortly, but it is nice to also give finer grained control.

It’s important to compare message() to the closely related cat(). In terms of usage and result, they appear quite similar47:

However, the purposes of cat() and message() are different. Use cat() when the primary role of the function is to print to the console, like print() or str() methods. Use message() as a side-channel to print to the console when the primary purpose of the function is something else. In other words, cat() is for when the user asks for something to be printed and message() is for when the developer elects to print something.

Exercises

  1. Write a wrapper around file.remove() that throws an error if the file
    to be deleted does not exist.

  2. What does the appendLF argument to message() do? How is it related to
    cat()?

Ignoring conditions

The simplest way of handling conditions in R is to simply ignore them:

  • Ignore errors with try().
  • Ignore warnings with suppressWarnings().
  • Ignore messages with suppressMessages().

These functions are heavy handed as you can’t use them to suppress a single type of condition that you know about, while allowing everything else to pass through. We’ll come back to that challenge later in the chapter.

try() allows execution to continue even after an error has occurred. Normally if you run a function that throws an error, it terminates immediately and doesn’t return a value:

f1 <- function(x) {
  log(x)
  10
}
f1("x")
#> Error in log(x): non-numeric argument to mathematical function

However, if you wrap the statement that creates the error in try(), the error message will be displayed48 but execution will continue:

f2 <- function(x) {
  try(log(x))
  10
}
f2("a")
#> Error in log(x) : non-numeric argument to mathematical function
#> [1] 10

It is possible, but not recommended, to save the result of try() and perform different actions based on whether or not the code succeeded or failed49. Instead, it is better to use tryCatch() or a higher-level helper; you’ll learn about those shortly.

A simple, but useful, pattern is to do assignment inside the call: this lets you define a default value to be used if the code does not succeed. This works because the argument is evaluated in the calling environment, not inside the function. (See Section 6.5.1 for more details.)

default <- NULL
try(default <- read.csv("possibly-bad-input.csv"), silent = TRUE)

suppressWarnings() and suppressMessages() suppress all warnings and messages. Unlike errors, messages and warnings don’t terminate execution, so there may be multiple warnings and messages signalled in a single block.

Handling conditions

Every condition has default behaviour: errors stop execution and return to the top level, warnings are captured and displayed in aggregate, and messages are immediately displayed. Condition handlers allow us to temporarily override or supplement the default behaviour.

Two functions, tryCatch() and withCallingHandlers(), allow us to register handlers, functions that take the signalled condition as their single argument. The registration functions have the same basic form:

tryCatch(
  error = function(cnd) {
    # code to run when error is thrown
  },
  code_to_run_while_handlers_are_active
)

withCallingHandlers(
  warning = function(cnd) {
    # code to run when warning is signalled
  },
  message = function(cnd) {
    # code to run when message is signalled
  },
  code_to_run_while_handlers_are_active
)

They differ in the type of handlers that they create:

  • tryCatch() defines exiting handlers; after the condition is handled,
    control returns to the context where tryCatch() was called. This makes
    tryCatch() most suitable for working with errors and interrupts, as these
    have to exit anyway.

  • withCallingHandlers() defines calling handlers; after the condition
    is captured control returns to the context where the condition was signalled.
    This makes it most suitable for working with non-error conditions.

But before we can learn about and use these handlers, we need to talk a little bit about condition objects. These are created implicitly whenever you signal a condition, but become explicit inside the handler.

Condition objects

So far we’ve just signalled conditions, and not looked at the objects that are created behind the scenes. The easiest way to see a condition object is to catch one from a signalled condition. That’s the job of rlang::catch_cnd():

cnd <- catch_cnd(stop("An error"))
str(cnd)
#> List of 2
#>  $ message: chr "An error"
#>  $ call   : language force(expr)
#>  - attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

Built-in conditions are lists with two elements:

  • message, a length-1 character vector containing the text to display to a user.
    To extract the message, use conditionMessage(cnd).

  • call, the call which triggered the condition. As described above, we don’t
    use the call, so it will often be NULL. To extract it, use
    conditionCall(cnd).

Custom conditions may contain other components, which we’ll discuss in Section 8.5.

Conditions also have a class attribute, which makes them S3 objects. We won’t discuss S3 until Chapter 13, but fortunately, even if you don’t know about S3, condition objects are quite simple. The most important thing to know is that the class attribute is a character vector, and it determines which handlers will match the condition.

Exiting handlers

tryCatch() registers exiting handlers, and is typically used to handle error conditions. It allows you to override the default error behaviour. For example, the following code will return NA instead of throwing an error:

f3 <- function(x) {
  tryCatch(
    error = function(cnd) NA,
    log(x)
  )
}

f3("x")
#> [1] NA

If no conditions are signalled, or the class of the signalled condition does not match the handler name, the code executes normally:

tryCatch(
  error = function(cnd) 10,
  1 + 1
)
#> [1] 2

tryCatch(
  error = function(cnd) 10,
  {
    message("Hi!")
    1 + 1
  }
)
#> Hi!
#> [1] 2

The handlers set up by tryCatch() are called exiting handlers because after the condition is signalled, control passes to the handler and never returns to the original code, effectively meaning that the code exits:

tryCatch(
  message = function(cnd) "There",
  {
    message("Here")
    stop("This code is never run!")
  }
)
#> [1] "There"

The protected code is evaluated in the environment of tryCatch(), but the handler code is not, because the handlers are functions. This is important to remember if you’re trying to modify objects in the parent environment.

The handler functions are called with a single argument, the condition object. I call this argument cnd, by convention. This value is only moderately useful for the base conditions because they contain relatively little data. It’s more useful when you make your own custom conditions, as you’ll see shortly.

tryCatch() has one other argument: finally. It specifies a block of code (not a function) to run regardless of whether the initial expression succeeds or fails. This can be useful for clean up, like deleting files, or closing connections. This is functionally equivalent to using on.exit() (and indeed that’s how it’s implemented) but it can wrap smaller chunks of code than an entire function.

Calling handlers

The handlers set up by tryCatch() are called exiting handlers, because they cause code to exit once the condition has been caught. By contrast, withCallingHandlers() sets up calling handlers: code execution continues normally once the handler returns. This tends to make withCallingHandlers() a more natural pairing with the non-error conditions. Exiting and calling handlers use “handler” in slighty different senses:

  • An exiting handler handles a signal like you handle a problem; it makes the
    problem go away.

  • A calling handler handles a signal like you handle a car; the car still
    exists.

Compare the results of tryCatch() and withCallingHandlers() in the example below. The messages are not printed in the first case, because the code is terminated once the exiting handler completes. They are printed in the second case, because a calling handler does not exit.

tryCatch(
  message = function(cnd) cat("Caught a message!n"), 
  {
    message("Someone there?")
    message("Why, yes!")
  }
)
#> Caught a message!

withCallingHandlers(
  message = function(cnd) cat("Caught a message!n"), 
  {
    message("Someone there?")
    message("Why, yes!")
  }
)
#> Caught a message!
#> Someone there?
#> Caught a message!
#> Why, yes!

Handlers are applied in order, so you don’t need to worry about getting caught in an infinite loop. In the following example, the message() signalled by the handler doesn’t also get caught:

(But beware if you have multiple handlers, and some handlers signal conditions that could be captured by another handler: you’ll need to think through the order carefully.)

The return value of a calling handler is ignored because the code continues to execute after the handler completes; where would the return value go? That means that calling handlers are only useful for their side-effects.

One important side-effect unique to calling handlers is the ability to muffle the signal. By default, a condition will continue to propagate to parent handlers, all the way up to the default handler (or an exiting handler, if provided):

If you want to prevent the condition “bubbling up” but still run the rest of the code in the block, you need to explicitly muffle it with rlang::cnd_muffle():

Call stacks

To complete the section, there are some important differences between the call stacks of exiting and calling handlers. These differences are generally not important but I’m including them here because I’ve occasionally found them useful, and don’t want to forget about them!

It’s easiest to see the difference by setting up a small example that uses lobstr::cst():

f <- function() g()
g <- function() h()
h <- function() message("!")

Calling handlers are called in the context of the call that signalled the condition:

withCallingHandlers(f(), message = function(cnd) {
  lobstr::cst()
  cnd_muffle(cnd)
})
#>      █
#>   1. ├─base::withCallingHandlers(...)
#>   2. ├─global::f()
#>   3. │ └─global::g()
#>   4. │   └─global::h()
#>   5. │     └─base::message("!")
#>   6. │       ├─base::withRestarts(...)
#>   7. │       │ └─base:::withOneRestart(expr, restarts[[1L]])
#>   8. │       │   └─base:::doWithOneRestart(return(expr), restart)
#>   9. │       └─base::signalCondition(cond)
#>  10. └─(function (cnd) ...
#>  11.   └─lobstr::cst()

Whereas exiting handlers are called in the context of the call to tryCatch():

tryCatch(f(), message = function(cnd) lobstr::cst())
#>     █
#>  1. └─base::tryCatch(f(), message = function(cnd) lobstr::cst())
#>  2.   └─base:::tryCatchList(expr, classes, parentenv, handlers)
#>  3.     └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
#>  4.       └─value[[3L]](cond)
#>  5.         └─lobstr::cst()

Exercises

  1. What extra information does the condition generated by abort() contain
    compared to the condition generated by stop() i.e. what’s the difference
    between these two objects? Read the help for ?abort to learn more.

  2. Predict the results of evaluating the following code

    show_condition <- function(code) {
      tryCatch(
        error = function(cnd) "error",
        warning = function(cnd) "warning",
        message = function(cnd) "message",
        {
          code
          NULL
        }
      )
    }
    
    show_condition(stop("!"))
    show_condition(10)
    show_condition(warning("?!"))
    show_condition({
      10
      message("?")
      warning("?!")
    })
  3. Explain the results of running this code:

  4. Read the source code for catch_cnd() and explain how it works.

  5. How could you rewrite show_condition() to use a single handler?

Custom conditions

One of the challenges of error handling in R is that most functions generate one of the built-in conditions, which contain only a message and a call. That means that if you want to detect a specific type of error, you can only work with the text of the error message. This is error prone, not only because the message might change over time, but also because messages can be translated into other languages.

Fortunately R has a powerful, but little used feature: the ability to create custom conditions that can contain additional metadata. Creating custom conditions is a little fiddly in base R, but rlang::abort() makes it very easy as you can supply a custom .subclass and additional metadata.

The following example shows the basic pattern. I recommend using the following call structure for custom conditions. This takes advantage of R’s flexible argument matching so that the name of the type of error comes first, followed by the user-facing text, followed by custom metadata.

abort(
  "error_not_found",
  message = "Path `blah.csv` not found", 
  path = "blah.csv"
)
#> Error: Path `blah.csv` not found

Custom conditions work just like regular conditions when used interactively, but allow handlers to do much more.

Motivation

To explore these ideas in more depth, let’s take base::log(). It does the minimum when throwing errors caused by invalid arguments:

log(letters)
#> Error in log(letters): non-numeric argument to mathematical function
log(1:10, base = letters)
#> Error in log(1:10, base = letters): non-numeric argument to mathematical
#> function

I think we can do better by being explicit about which argument is the problem (i.e. x or base), and saying what the problematic input is (not just what it isn’t).

This gives us:

my_log(letters)
#> Error: `x` must be a numeric vector; not character.
my_log(1:10, base = letters)
#> Error: `base` must be a numeric vector; not character.

This is an improvement for interactive usage as the error messages are more likely to guide the user towards a correct fix. However, they’re no better if you want to programmatically handle the errors: all the useful metadata about the error is jammed into a single string.

Signalling

Let’s build some infrastructure to improve this situation, We’ll start by providing a custom abort() function for bad arguments. This is a little over-generalised for the example at hand, but it reflects common patterns that I’ve seen across other functions. The pattern is fairly simple. We create a nice error message for the user, using glue::glue(), and store metadata in the condition call for the developer.

abort_bad_argument <- function(arg, must, not = NULL) {
  msg <- glue::glue("`{arg}` must {must}")
  if (!is.null(not)) {
    not <- typeof(not)
    msg <- glue::glue("{msg}; not {not}.")
  }
  
  abort("error_bad_argument", 
    message = msg, 
    arg = arg, 
    must = must, 
    not = not
  )
}

If you want to throw a custom error without adding a dependency on rlang, you can create a condition object “by hand” and then pass it to stop():

stop_custom <- function(.subclass, message, call = NULL, ...) {
  err <- structure(
    list(
      message = message,
      call = call,
      ...
    ),
    class = c(.subclass, "error", "condition")
  )
  stop(err)
}

err <- catch_cnd(
  stop_custom("error_new", "This is a custom error", x = 10)
)
class(err)
err$x

We can now rewrite my_log() to use this new helper:

my_log <- function(x, base = exp(1)) {
  if (!is.numeric(x)) {
    abort_bad_argument("x", must = "be numeric", not = x)
  }
  if (!is.numeric(base)) {
    abort_bad_argument("base", must = "be numeric", not = base)
  }

  base::log(x, base = base)
}

my_log() itself is not much shorter, but is a little more meangingful, and it ensures that error messages for bad arguments are consistent across functions. It yields the same interactive error messages as before:

my_log(letters)
#> Error: `x` must be numeric; not character.
my_log(1:10, base = letters)
#> Error: `base` must be numeric; not character.

Handling

These structured condition objects are much easier to program with. The first place you might want to use this capability is when testing your function. Unit testing is not a subject of this book (see R packages for details), but the basics are easy to understand. The following code captures the error, and then asserts it has the structure that we expect.

We can also use the class (error_bad_argument) in tryCatch() to only handle that specific error:

tryCatch(
  error_bad_argument = function(cnd) "bad_argument",
  error = function(cnd) "other error",
  my_log("a")
)
#> [1] "bad_argument"

When using tryCatch() with multiple handlers and custom classes, the first handler to match any class in the signal’s class vector is called, not the best match. For this reason, you need to make sure to put the most specific handlers first. The following code does not do what you might hope:

tryCatch(
  error = function(cnd) "other error",
  error_bad_argument = function(cnd) "bad_argument",
  my_log("a")
)
#> [1] "other error"

Exercises

  1. Inside a package, it’s occasionally useful to check that a package is
    installed before using it. Write a function that checks if a package is
    installed (with requireNamespace("pkg", quietly = FALSE)) and if not,
    throws a custom condition that includes the package name in the metadata.

  2. Inside a package you often need to stop with an error when something
    is not right. Other packages that depend on your package might be
    tempted to check these errors in their unit tests. How could you help
    these packages to avoid relying on the error message which is part of
    the user interface rather than the API and might change without notice?

Applications

Now that you’ve learned the basic tools of R’s condition system, it’s time to dive into some applications. The goal of this section is not to show every possible usage of tryCatch() and withCallingHandlers() but to illustrate some common patterns that frequently crop up. Hopefully these will get your creative juices flowing, so when you encounter a new problem you can come up with a useful solution.

Failure value

There are a few simple, but useful, tryCatch() patterns based on returning a value from the error handler. The simplest case is a wrapper to return a default value if an error occurs:

fail_with <- function(expr, value = NULL) {
  tryCatch(
    error = function(cnd) value,
    expr
  )
}

fail_with(log(10), NA_real_)
#> [1] 2.3
fail_with(log("x"), NA_real_)
#> [1] NA

A more sophisticated application is base::try(). Below, try2() extracts the essence of base::try(); the real function is more complicated in order to make the error message look more like what you’d see if tryCatch() wasn’t used.

try2 <- function(expr, silent = FALSE) {
  tryCatch(
    error = function(cnd) {
      msg <- conditionMessage(cnd)
      if (!silent) {
        message("Error: ", msg)
      }
      structure(msg, class = "try-error")
    },
    expr
  )
}

try2(1)
#> [1] 1
try2(stop("Hi"))
#> Error: Hi
#> [1] "Hi"
#> attr(,"class")
#> [1] "try-error"
try2(stop("Hi"), silent = TRUE)
#> [1] "Hi"
#> attr(,"class")
#> [1] "try-error"

Success and failure values

We can extend this pattern to return one value if the code evaluates successfully (success_val), and another if it fails (error_val). This pattern just requires one small trick: evaluating the user supplied code, then success_val. If the code throws an error, we’ll never get to success_val and will instead return error_val.

foo <- function(expr) {
  tryCatch(
    error = function(cnd) error_val,
    {
      expr
      success_val
    }
  )
}

We can use this to determine if an expression fails:

does_error <- function(expr) {
  tryCatch(
    error = function(cnd) TRUE,
    {
      expr
      FALSE
    }
  )
}

Or to capture any condition, like just rlang::catch_cnd():

catch_cnd <- function(expr) {
  tryCatch(
    condition = function(cnd) cnd, 
    {
      expr
      NULL
    }
  )
}

We can also use this pattern to create a try() variant. One challenge with try() is that it’s slightly challenging to determine if the code succeeded or failed. Rather than returning an object with a special class, I think it’s slightly nicer to return a list with two components result and error.

safety <- function(expr) {
  tryCatch(
    error = function(cnd) {
      list(result = NULL, error = cnd)
    },
    list(result = expr, error = NULL)
  )
}

str(safety(1 + 10))
#> List of 2
#>  $ result: num 11
#>  $ error : NULL
str(safety(stop("Error!")))
#> List of 2
#>  $ result: NULL
#>  $ error :List of 2
#>   ..$ message: chr "Error!"
#>   ..$ call   : language doTryCatch(return(expr), name, parentenv, handler)
#>   ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

(This is closely related to purrr::safely(), a function operator, which we’ll come back to in Section 11.2.1.)

Resignal

As well as returning default values when a condition is signalled, handlers can be used to make more informative error messages. One simple application is to make a function that works like options(warn = 2) for a single block of code. The idea is simple: we handle warnings by throwing an error:

warning2error({
  x <- 2 ^ 4
  warn("Hello")
})
#> Error: Hello

You could write a similar function if you were trying to find the source of an annoying message. More on this in Section 22.6.

Record

Another common pattern is to record conditions for later investigation. The new challenge here is that calling handlers are called only for their side-effects so we can’t return values, but instead need to modify some object in place.

catch_cnds <- function(expr) {
  conds <- list()
  add_cond <- function(cnd) {
    conds <<- append(conds, list(cnd))
    cnd_muffle(cnd)
  }
  
  withCallingHandlers(
    message = add_cond,
    warning = add_cond,
    expr
  )
  
  conds
}

catch_cnds({
  inform("a")
  warn("b")
  inform("c")
})
#> [[1]]
#> <message: a
#> >
#> 
#> [[2]]
#> <warning: b>
#> 
#> [[3]]
#> <message: c
#> >

What if you also want to capture errors? You’ll need to wrap the withCallingHandlers() in a tryCatch(). If an error occurs, it will be the last condition.

catch_cnds <- function(expr) {
  conds <- list()
  add_cond <- function(cnd) {
    conds <<- append(conds, list(cnd))
    cnd_muffle(cnd)
  }
  
  tryCatch(
    error = function(cnd) {
      conds <<- append(conds, list(cnd))
    },
    withCallingHandlers(
      message = add_cond,
      warning = add_cond,
      expr
    )
  )
  
  conds
}

catch_cnds({
  inform("a")
  warn("b")
  abort("C")
})
#> [[1]]
#> <message: a
#> >
#> 
#> [[2]]
#> <warning: b>
#> 
#> [[3]]
#> <error/rlang_error>
#> C
#> Backtrace:
#>  1. global::catch_cnds(...)
#>  6. base::withCallingHandlers(...)

This is the key idea underlying the evaluate package50 which powers knitr: it captures every output into a special data structure so that it can be later replayed. As a whole, the evaluate package is quite a lot more complicated than the code here because it also needs to handle plots and text output.

No default behaviour

A final useful pattern is to signal a condition that doesn’t inherit from message, warning or error. Because there is no default behaviour, this means the condition has no effect unless the user specifically requests it. For example, you could imagine a logging system based on conditions:

log <- function(message, level = c("info", "error", "fatal")) {
  level <- match.arg(level)
  signal(message, "log", level = level)
}

When you call log() a condition is signalled, but nothing happens because it has no default handler:

To activate logging you need a handler that does something with the log condition. Below I define a record_log() function that will record all logging messages to a file:

record_log <- function(expr, path = stdout()) {
  withCallingHandlers(
    log = function(cnd) {
      cat(
        "[", cnd$level, "] ", cnd$message, "n", sep = "",
        file = path, append = TRUE
      )
    },
    expr
  )
}

record_log(log("Hello"))
#> [info] Hello

You could even imagine layering with another function that allows you to selectively suppress some logging levels.

ignore_log_levels <- function(expr, levels) {
  withCallingHandlers(
    log = function(cnd) {
      if (cnd$level %in% levels) {
        cnd_muffle(cnd)
      }
    },
    expr
  )
}

record_log(ignore_log_levels(log("Hello"), "info"))

If you create a condition object by hand, and signal it with signalCondition(), cnd_muffle() will not work. Instead you need to call it with a muffle restart defined, like this:

Restarts are currently beyond the scope of the book, but I suspect will be included in the third edition.

Exercises

  1. Create suppressConditions() that works like suppressMessages() and
    suppressWarnings() but suppresses everything. Think carefully about how you
    should handle errors.

  2. Compare the following two implementations of message2error(). What is the
    main advantage of withCallingHandlers() in this scenario? (Hint: look
    carefully at the traceback.)

  3. How would you modify the catch_cnds() definition if you wanted to recreate
    the original intermingling of warnings and messages?

  4. Why is catching interrupts dangerous? Run this code to find out.

    bottles_of_beer <- function(i = 99) {
      message(
        "There are ", i, " bottles of beer on the wall, ", 
        i, " bottles of beer."
      )
      while(i > 0) {
        tryCatch(
          Sys.sleep(1),
          interrupt = function(err) {
            i <<- i - 1
            if (i > 0) {
              message(
                "Take one down, pass it around, ", i, 
                " bottle", if (i > 1) "s", " of beer on the wall."
              )
            }
          }
        )
      }
      message(
        "No more bottles of beer on the wall, ", 
        "no more bottles of beer."
      )
    }

Quiz answers

  1. error, warning, and message.

  2. You could use try() or tryCatch().

  3. tryCatch() creates exiting handlers which will terminate the execution
    of wrapped code; withCallingHandlers() creates calling handlers which
    don’t affect the execution of wrapped code.

  4. Because you can then capture specific types of error with tryCatch(),
    rather than relying on the comparison of error strings, which is risky,
    especially when messages are translated.

Понравилась статья? Поделить с друзьями:
  • Quote command returned error фтп 1с переместить
  • Quote command returned error перевод
  • Quota exceeded error перевод
  • Quixel bridge blender export error
  • Quik ошибка 810