How and when should I use on.exit?

RR Faq

R Problem Overview


on.exit calls code when a function exits, but how and when should I use it?

R Solutions


Solution 1 - R

The advantage of on.exit is that is gets called when the function exits, regardless of whether an error was thrown. This means that its main use is for cleaning up after risky behaviour. Risky, in this context, usually means accessing resources outside of R (that consequently cannot be guaranteed to work). Common examples include connecting to databases or file (where the connection must be closed when you are finished, even if there was an error), or saving a plot to a file (where the graphics device must be closed afterwards).

You can also use on.exit for low-risk behaviour with a side effect, such as setting a working directory.

In general, you should set add = TRUE in on.exit(). See https://adv-r.hadley.nz/functions.html?q=on.exit#on-exit.


Packages that make use of on.exit

The withr package contains many with_* functions that change a setting, run some code, then change the setting back. These functions also appear in the devtools package.

An alternate syntax is found in the later package where defer is a convenience wrapper to on.exit, and scope_* functions work like the with_* functions in the previously mentioned packages.


Database connections

In this example, sqlite_get_query connects to an sqlite database, ensuring that the connection always gets closed after the query has run. The cookies database requires that you have firefox installed on your machine, and you may need to adjust the path to find the cookies file.

library(RSQLite)
sqlite_get_query <- function(db, sql)
{
  conn <- dbConnect(RSQLite::SQLite(), db)
  on.exit(dbDisconnect(conn), add = TRUE)
  dbGetQuery(conn, sql)
}

cookies <- dir(
  file.path(Sys.getenv("APPDATA"), "Mozilla", "Firefox"), 
  recursive  = TRUE, 
  pattern    = "cookies.sqlite$",
  full.names = TRUE
)[1]

sqlite_get_query(
  cookies, 
  "SELECT `baseDomain`, `name`, `value` FROM moz_cookies LIMIT 20"
)

File connections

In this example, read_chars wraps readChars, ensuring that the connection to the file is always closed after reading is finished.

read_chars <- function(file_name)
{
  conn <- file(file_name, "r")
  on.exit(close(conn), add = TRUE)
  readChar(conn, file.info(file_name)$size)
}

tmp <- tempfile()
cat(letters, file = tmp, sep = "")
read_chars(tmp)

Temporary files

The following example adapted from CodeDepends uses a temporary file to save the session history. This temporary file is not needed once the function returns so it is removed.

history_lines <- function()
{
  f <- tempfile()
  on.exit(unlink(f), add = TRUE)
  savehistory(f)
  readLines(f, encoding = "UTF-8")
}

Saving base graphics

In this example, my_plot is a function that creates a plot using base graphics. save_base_plot accepts a function and a file to save it to, using on.exit to ensure that the graphics device is always closed.

my_plot <- function()
{
  with(cars, plot(speed, dist))
}

save_base_plot <- function(plot_fn, file)
{
  png(file)
  on.exit(dev.off(), add = TRUE)
  plot_fn()
}

save_base_plot(my_plot, "testcars.png")

Setting base graphics options temporarily

In this example, plot_with_big_margins calls plot, overriding the global margin parameter, using on.exit to reset it after the plot is completed.

plot_with_big_margins <- function(...)
{
  old_pars <- par(mar = c(10, 9, 9, 7))  
  on.exit(par(old_pars), add = TRUE)
  plot(...)
}

plot_with_big_margins(with(cars, speed, dist))

withr/devtools equivalent: with_par


Setting global options temporarily

In this example, create_data_frame is a function that creates a data.frame. create_data_frame ensures that the created object doesn't contain explicit factors.

create_data_frame <- function(){
  op <- options(stringsAsFactors = FALSE)
  on.exit(options(op), add = TRUE)
  
  data.frame(x=1:10)
}

withr/devtools equivalent: with_options
later equivalent: scope_options


Other examples
  • Setting the working directory (withr::with_dir, later::scope_dir)

  • Setting locale components (withr::with_locale)

  • Setting environment variables (withr::with_envvars, later::scope_env_var)

  • Setting library paths (withr::with_libpaths)

  • Redirecting output with a sink

  • Temporarily loading a package (withr::with_package, withr::with_namespace)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRichie CottonView Question on Stackoverflow
Solution 1 - RRichie CottonView Answer on Stackoverflow