6.7 Exiting a function

Most functions exit in one of two ways29: they either return a value, indicating success, or they throw an error, indicating failure. This section describes return values (implicit versus explicit; visible versus invisible), briefly discusses errors, and introduces exit handlers, which allow you to run code when a function exits.

6.7.1 Implicit versus explicit returns

There are two ways that a function can return a value:

  • Implicitly, where the last evaluated expression is the return value:

    j01 <- function(x) {
      if (x < 10) {
        0
      } else {
        10
      }
    }
    j01(5)
    #> [1] 0
    j01(15)
    #> [1] 10
  • Explicitly, by calling return():

    j02 <- function(x) {
      if (x < 10) {
        return(0)
      } else {
        return(10)
      }
    }

6.7.2 Invisible values

Most functions return visibly: calling the function in an interactive context prints the result.

j03 <- function() 1
j03()
#> [1] 1

However, you can prevent automatic printing by applying invisible() to the last value:

j04 <- function() invisible(1)
j04()

To verify that this value does indeed exist, you can explicitly print it or wrap it in parentheses:

print(j04())
#> [1] 1

(j04())
#> [1] 1

Alternatively, you can use withVisible() to return the value and a visibility flag:

str(withVisible(j04()))
#> List of 2
#>  $ value  : num 1
#>  $ visible: logi FALSE

The most common function that returns invisibly is <-:

a <- 2
(a <- 2)
#> [1] 2

This is what makes it possible to chain assignments:

a <- b <- c <- d <- 2

In general, any function called primarily for a side effect (like <-, print(), or plot()) should return an invisible value (typically the value of the first argument).

6.7.3 Errors

If a function cannot complete its assigned task, it should throw an error with stop(), which immediately terminates the execution of the function.

j05 <- function() {
  stop("I'm an error")
  return(10)
}
j05()
#> Error in j05(): I'm an error

An error indicates that something has gone wrong, and forces the user to deal with the problem. Some languages (like C, Go, and Rust) rely on special return values to indicate problems, but in R you should always throw an error. You’ll learn more about errors, and how to handle them, in Chapter 8.

6.7.4 Exit handlers

Sometimes a function needs to make temporary changes to the global state. But having to cleanup those changes can be painful (what happens if there’s an error?). To ensure that these changes are undone and that the global state is restored no matter how a function exits, use on.exit() to set up an exit handler. The following simple example shows that the exit handler is run regardless of whether the function exits normally or with an error.

j06 <- function(x) {
  cat("Hello\n")
  on.exit(cat("Goodbye!\n"), add = TRUE)
  
  if (x) {
    return(10)
  } else {
    stop("Error")
  }
}

j06(TRUE)
#> Hello
#> Goodbye!
#> [1] 10

j06(FALSE)
#> Hello
#> Error in j06(FALSE): Error
#> Goodbye!

on.exit() is useful because it allows you to place clean-up code directly next to the code that requires clean-up:

cleanup <- function(dir, code) {
  old_dir <- setwd(dir)
  on.exit(setwd(old_dir), add = TRUE)
  
  old_opt <- options(stringsAsFactors = FALSE)
  on.exit(options(old_opt), add = TRUE)
}

Coupled with lazy evaluation, this creates a very useful pattern for running a block of code in an altered environment:

with_dir <- function(dir, code) {
  old <- setwd(dir)
  on.exit(setwd(old), add = TRUE)

  force(code)
}

getwd()
#> [1] "/root"
with_dir("~", getwd())
#> [1] "/root"

The use of force() isn’t strictly necessary here as simply referring to code will force its evaluation. However, using force() makes it very clear that we are deliberately forcing the execution. You’ll learn other uses of force() in Chapter 10.

The withr package (Hester et al. 2018) provides a collection of other functions for setting up a temporary state.

In R 3.4 and earlier, on.exit() expressions are always run in order of creation:

j08 <- function() {
  on.exit(message("a"), add = TRUE)
  on.exit(message("b"), add = TRUE)
}
j08()
#> a
#> b

This can make cleanup a little tricky if some actions need to happen in a specific order; typically you want the most recent added expression to be run first. In R 3.5 and later, you can control this by setting after = FALSE:

j09 <- function() {
  on.exit(message("a"), add = TRUE, after = FALSE)
  on.exit(message("b"), add = TRUE, after = FALSE)
}
j09()
#> b
#> a

6.7.5 Exercises

  1. What does load() return? Why don’t you normally see these values?

  2. What does write.table() return? What would be more useful?

  3. How does the chdir parameter of source() compare to with_dir()? Why might you prefer one to the other?

  4. Write a function that opens a graphics device, runs the supplied code, and closes the graphics device (always, regardless of whether or not the plotting code works).

  5. We can use on.exit() to implement a simple version of capture.output().

    capture.output2 <- function(code) {
      temp <- tempfile()
      on.exit(file.remove(temp), add = TRUE, after = TRUE)
    
      sink(temp)
      on.exit(sink(), add = TRUE, after = TRUE)
    
      force(code)
      readLines(temp)
    }
    capture.output2(cat("a", "b", "c", sep = "\n"))
    #> [1] "a" "b" "c"

    Compare capture.output() to capture.output2(). How do the functions differ? What features have I removed to make the key ideas easier to see? How have I rewritten the key ideas so they’re easier to understand?

References

Hester, Jim, Kirill Müller, Kevin Ushey, Hadley Wickham, and Winston Chang. 2018. Withr: Run Code with Temporarily Modified Global State. http://withr.r-lib.org.


  1. Functions can exit in other more esoteric ways like signalling a condition that is caught by an exit handler, invoking a restart, or pressing “Q” in an interactive browser.↩︎