After several hours of trying to fix a bug while deploying an R package (oh vignettes, don't get me started on this...), I had a closer look at the code, and tried to identify the things which would have made my life easier. And believe it or not, this are the little things...

Starting with: Have you ever used these functions?

file.copy(...)
file.remove(...)

They are trivial, I thought, so I never bothered to read the manual (who does it?)... until it stabbed me back... https://xkcd.com/293/

xkcd read the manual

It turns out, these functions actually try to do what they promise in the name, but do not fail if they do not succeed - which is not what I naively expected, as you can see here:


file.copy(
  from = "from-non-existing.txt",
  to = "to-non-existing.txt"
)
# FALSE

R.utils::copyFile(
  srcPathname = "from-non-existing.txt",
  destPathname = "to-non-existing.txt"
)
# Error: Failed to copy file. No such file: from-non-existing.txt
file.rename(
  from = "from-non-existing.txt",
  to = "to-non-existing.txt"
)
# FALSE
# Warning message:
# In file.rename(from = "from-non-existing.txt", to = "to-non-existing.txt") :
# cannot rename file 'from-non-existing.txt' to 'to-non-existing.txt', reason 'No such file or directory'

R.utils::renameFile(
  pathname = "from-non-existing.txt",
  newPathname = "to-non-existing.txt"
)
#Error: Pathname not found: from-non-existing.txt (no such file in the current working directory;
file.remove("non-existing.txt")
# FALSE
# Warning message:
#  In file.remove("non-existing.txt") :
#  cannot remove file 'non-existing.txt', reason 'No such file or directory'
# unlink(x = "non-existing.txt")

unlink(x = "non-existing-directory", recursive = TRUE)
# returns 0 for success, 1 for failure, invisibly
# not deleting a non-existing file is not a failure

R.utils::removeDirectory(path = "non-existing-directory")
# Error: Pathname not found: non-existing-directory (no such file in the current working directory;

I also included equivalent functions which fail immediately.

(Side remark: if you think - I'm sure my file is there, so it's safe to use file.copy. Think twice: yes, the file might be there. Now. Or most of the time. But how sure are you? - It happened to me, in a totally different package, just a few days later. My advice: don't hope for the best. Hope that when things get bad, they fail immediately...)

I prefer to use functions which fail fast and often, in the spirit of Test Driven Development. If you are new to this concept, you can find more on the internet.

In my case, if I would have used the proper function to copy a file, I would have much sooner realized that the problem was that the file did not exist... Bugs are bad, and the worse are the ones I introduce myself (where was I when I wrote this?).  There are countless sources which state that is much cheaper to fix bugs as soon as possible, for example here: The true cost of a software bug .

I see two possible solutions:

On the spirit of knowing your tools, here is another example about the on.exit function. This is  a function which runs some code when the current functions exits (either normal exit, or as the result of an error). This function can be for example used for cleanup (delete unused files, close database connections, etc). Nowadays there are better ways of doing this, namely using withr::defer(). Please see here for a gentle introduction: R testthat test fixtures .

But if are stuck with legacy code using on.exit, you might need to make sure the right arguments are used. It turns out that the default values for these functions' arguments are actually the opposite of what you normally want to have:

# Note: the second on.exit overwrites the first one
feed_me <- function() {
  on.exit(
    message("put the food in the dishwasher")
  )
  
  message("prepare some tasty food")
  
  on.exit(
    message("put the food on the plate")
  )
}

feed_me()
# prepare some tasty food
# put the food on the plate
# with add = TRUE, both on.exit calls are executed, but in "wrong" order
feed_me <- function() {
  on.exit(
    message("put the food in the dishwasher"), add = TRUE
  )
  
  message("prepare some tasty food")
  
  on.exit(
    message("put the food on the plate"), add = TRUE
  )
}
feed_me()
# prepare some tasty food
# put the food in the dishwasher
# put the food on the plate
# with add = TRUE and after = FALSE you are (probably most of the time) ok
feed_me <- function() {
  on.exit(
    message("put the plate in the dishwasher"), add = TRUE, after = FALSE
  )
  
  message("prepare some tasty food")
  
  on.exit(
    message("put the food on the plate"), add = TRUE, after = FALSE
  )
}
feed_me()
# prepare some tasty food
# put the food on the plate
# put the plate in the dishwasher

This article is the starting point in a series about writing robust code in R. Yes, I know, my opinions are biased and sometimes too strong. I'm happy to hear your thoughts on this, and to improve the articles here, such that we all spent less time on hunting bugs, and more time on writing code that works (and that we can be proud about).

Make a promise. Show up. Do the work. Repeat.