Catching, passing, handling errors

Author

Galen Holt

There’s a lot out there on handling errors. This will mostly be testing things as they come up when I need to use them. Will rely heavily on Hadley, as usual.

Returning value or the error

I want to return the output value if something works, or the error message if it doesn’t. Let’s say in a list.

I don’t like how all the demos include an explicit stop() call, somehow that confuses me. I’m going to do essentially the same thing, but bury it in a function, so it’s more like the behaviour we’d actually see.

err_even <- function(x) {
  if ((x %% 2) == 0) {stop('Even numbers are error')} else {x}
  }

As a test, what does that look over a vector? I’m going to loop so it’s clearer (maybe). And so I don’t get an error about asking about a vector in an if.

outvec <- vector(mode = 'numeric', length = 10)
for (i in 1:10) {outvec[i] <- err_even(i)}
Error in err_even(i): Even numbers are error

Try and passing

If we just use try, the error should get printed but everything keeps moving

outvec <- vector(mode = 'numeric', length = 10)
for (i in 1:10) {outvec[i] <- try(err_even(i))}
Error in err_even(i) : Even numbers are error
Error in err_even(i) : Even numbers are error
Error in err_even(i) : Even numbers are error
Error in err_even(i) : Even numbers are error
Error in err_even(i) : Even numbers are error
outvec
 [1] "1"                                              
 [2] "Error in err_even(i) : Even numbers are error\n"
 [3] "3"                                              
 [4] "Error in err_even(i) : Even numbers are error\n"
 [5] "5"                                              
 [6] "Error in err_even(i) : Even numbers are error\n"
 [7] "7"                                              
 [8] "Error in err_even(i) : Even numbers are error\n"
 [9] "9"                                              
[10] "Error in err_even(i) : Even numbers are error\n"

Huh. I thought try just printed the values but let things keep going. Changing the non-failures to character isn’t ideal. But I guess then I’d use tryCatch? For now though, this is exactly what i need, so I’ll stop here.

tryCatch

I actually want to capture errors, warnings, or passing to assess some code

err_even_warn5 <- function(x) {
  if ((x %% 2) == 0) {
    stop('Even numbers are error')
  } else if (x == 5) {
    warning('5 throws a warning')
  } else {x}
}

I want to use this for recording, so

recorder <- vector(mode = 'character', length = 10)
for (i in 1:10) {
  recorder[i] <- tryCatch(err_even_warn5(i),
                        error = function(c) c$message,
                        warning = function(c) c$message,
                        message = function(c) c$message)
}
recorder
 [1] "1"                      "Even numbers are error" "3"                     
 [4] "Even numbers are error" "5 throws a warning"     "Even numbers are error"
 [7] "7"                      "Even numbers are error" "9"                     
[10] "Even numbers are error"

And to be even more explicit, can I do some mods in the call to just say if it passed?

recorder2 <- vector(mode = 'character', length = 10)
for (i in 1:10) {
  recorder2[i] <- tryCatch(if(is.numeric(err_even_warn5(i))) {'pass'},
                        error = function(c) c$message,
                        warning = function(c) c$message,
                        message = function(c) c$message)
}
recorder2
 [1] "pass"                   "Even numbers are error" "pass"                  
 [4] "Even numbers are error" "5 throws a warning"     "Even numbers are error"
 [7] "pass"                   "Even numbers are error" "pass"                  
[10] "Even numbers are error"

Now, for packages, sometimes I want to ignore certain warnings and messages that I know are OK, while letting other unexpected ones bubble up. I think according to Hadley here and here, I want withCallingHandlers() rather than tryCatch because I want the code flow to proceed unimpeded.

So, let’s say I have a function that throws ‘expected’ warnings and messages and ‘unexpected’ ones, as well as errors (which should still error).

expect_unexpect <- function(x) {
  if ((x == 10)) {
    stop('10 is error')
  } else if (x == 2) {
    warning('expected warning at 2')
  } else if (x == 3) {
    warning('unexpected warning at 3')
  } else if (x == 4) {
    message('expected message at 4')
  } else if (x == 5) {
    message('unexpected message at 5')
  } else {x}
  
  x
}

So, how does withCallingHandlers work?

testvec <- 1:6*NA

for (i in 1:6) {
  testvec[i] <- withCallingHandlers(
    warning = function(cnd) {
      i *2
    }, 
    message = function(cnd) {
      i * 3
    },
    expect_unexpect(i)
  )
}
Warning in expect_unexpect(i): expected warning at 2
Warning in expect_unexpect(i): unexpected warning at 3
expected message at 4
unexpected message at 5
testvec
[1] 1 2 3 4 5 6

So that’s clearly doing what it should in that it passes the actual output, but the bits where it’s managing cnds needs work.

What I actually want to do is ignore those warnings and messages when they are expected.

What do the objects look like?

rlang::catch_cnd(expect_unexpect(4))
<simpleMessage in message("expected message at 4"): expected message at 4
>
rlang::catch_cnd(expect_unexpect(4))$message
[1] "expected message at 4\n"
rlang::catch_cnd(expect_unexpect(4))$call
message("expected message at 4")

Do I need that? or do I just need to do a check?

This muffles warnings but not messages

testvec <- 1:6*NA

for (i in 1:6) {
  testvec[i] <- withCallingHandlers(
    warning = function(cnd) {
        rlang::cnd_muffle(cnd)
    },
    message = function(cnd) {
      i
    },
    expect_unexpect(i)
  )
}
expected message at 4
unexpected message at 5
testvec
[1] 1 2 3 4 5 6

But how do I muffle only some warnings? I thought I needed rlang::catch_cnd(), but that’s an extra unnecessary layer- it’s null, the cnd being passed around here is already caught.

testvec <- 1:6*NA

for (i in 1:6) {
  testvec[i] <- withCallingHandlers(
    warning = function(cnd) {
      print(cnd)
      print(rlang::catch_cnd(cnd))
      print(cnd$message)
    },
    message = function(cnd) {
      i
    },
    expect_unexpect(i)
  )
}
<simpleWarning in expect_unexpect(i): expected warning at 2>
NULL
[1] "expected warning at 2"
Warning in expect_unexpect(i): expected warning at 2
<simpleWarning in expect_unexpect(i): unexpected warning at 3>
NULL
[1] "unexpected warning at 3"
Warning in expect_unexpect(i): unexpected warning at 3
expected message at 4
unexpected message at 5
testvec
[1] 1 2 3 4 5 6

These don’t have useful classes, so just use grep on the message. When I go to do this for real, will need to use rlang::catch_cnd() to look at what I’m dealin with and see if I can do a better condition.

testvec <- 1:6*NA

for (i in 1:6) {
  testvec[i] <- withCallingHandlers(
    warning = function(cnd) {
      if (grepl('^expect', cnd$message)) {
        rlang::cnd_muffle(cnd)
      }
    },
    message = function(cnd) {
      if (grepl('^expect', cnd$message)) {
        rlang::cnd_muffle(cnd)
      }
    },
    expect_unexpect(i)
  )
}
Warning in expect_unexpect(i): unexpected warning at 3
unexpected message at 5
testvec
[1] 1 2 3 4 5 6

Asides/specific cases

For purrr::map and similar functions, we can use purrr::safely and purrr::possibly to pass errors without failing a whole run. The output then needs to be unpacked and cleaned up.

For foreach::foreach, we can use the .errorhandling argument to pass errors through without failing the whole run.