err_even <- function(x) {
if ((x %% 2) == 0) {stop('Even numbers are error')} else {x}
}Catching, passing, handling errors
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.
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))$callmessage("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.