Lazy Evaluation and Promises
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. Everything on this page is base R, so no
package versions apply: the packages that are named in the prose (rlang, lobstr, pryr) are community material and none of them
is run. Every [source,r] block is part of a script that was run with Rscript on R 4.6.1 with warnings printed as they happen
(options(warn = 1)) and that passes the default lintr linters; the lines that start with #> are what it printed, in the
order it printed them (messages and normal output interleaved). A script that is cut into several blocks with text between them
is one script: run its blocks in order. Where a line does exactly what a default linter dislikes, it carries a # nolint:
comment so that the linter does not hide the lesson. The names books, loans, shelf_today, loan_days and the book class
come from the scenario.R file of the running library-shelf scenario described on the
section index; scripts that use them start with source("scenario.R").
Calling a function in R does not evaluate its arguments. Each argument is wrapped in a promise, a small object that remembers the
expression the caller wrote and the environment it has to be evaluated in, and the function evaluates it, once, if and when it
uses the argument. That one decision explains why a default argument can refer to a variable created later in the body, why
missing() and substitute() work, why tryCatch(expr, ...) can catch an error that is raised in the code you typed as
expr, why a closure made in a for loop can see the wrong value, and why a message() placed inside an argument sometimes
never prints. This page takes the mechanism apart, shows each consequence with a script, and lists the real error and warning
messages it produces. It builds on Functions (argument matching and defaults) and
Scoping, Environments and Closures (environments, frames and
lexical scope); the function-factory part continues on
Function Factories and Higher-Order Functions.
Why R evaluates arguments lazily
R’s argument passing is call by value with lazy evaluation. The Language Definition states both halves in one section: supplied arguments behave as if they were local variables initialised with the value supplied, so changing one inside the function does not change the variable in the calling frame, and yet the arguments are not evaluated until needed, so in some cases an argument is never evaluated at all (R-lang: Argument evaluation). The technical name for the combination is call by need: an argument is evaluated at most once, on demand, and its value is kept.
Four features of the language rest on that design.
-
Defaults that depend on the other arguments or on the body. A default is an expression, not a value fixed when the function is defined, and it is evaluated in the function’s own frame, so it can use any argument and any local variable that exists by the time it is needed.
-
Optional and absent arguments. A formal argument that was not supplied only becomes an error if the body evaluates it, so a function can accept fewer arguments than it declares, and
missing()can ask whether the caller supplied one. -
Access to what the caller typed. Because the promise keeps the expression,
substitute()can return it. This is the basis of the default axis labels ofplot(), and of the non-standard evaluation insubset(),with()and the tidyverse; see Language Objects and Quoting and Evaluation and Non-Standard Evaluation. -
Functions that look like control flow.
tryCatch(),suppressWarnings(),system.time(),local()andon.exit()receive the code they guard as an unevaluated argument and choose where and when to run it. A language with eager arguments would need a special syntax or a closure for each of them.
The price is that the order and the number of evaluations are decided by the function body, not by the call, so side effects placed in an argument are unreliable. The manual says so plainly: it is bad style to use arguments to cause side effects, because nothing guarantees that the argument is ever evaluated (R-lang: Argument evaluation). The sections below show where this bites and how to write code that does not depend on it.
The R FAQ records one place where this design differs from S: S’s substitute() looked for names in the actual and default
arguments of the matching call and in the local frame, whereas R looks in the local frame only, with a special rule that uses the
promise when the variable has not been evaluated yet
(R FAQ 3.3.3: Others). The two agree until an assignment takes place, which
is exactly what the substitute() section below shows.
The anatomy of a promise
A promise has three slots: an expression, an environment and a value
(R-lang: Promise objects). The internals manual
describes the same object as the PROMSXP type: pointers to the value, the expression and the environment in which to evaluate
the expression, with the remark that once a promise has been evaluated, the environment is set to NULL
(R-ints: The data). The expression and the
environment are filled in when the promise is created, the value when it is first needed.
Promises are created in four places.
-
When a closure is called. Argument matching replaces the list of actual arguments with a list of promises, and a new environment is built whose bindings are those promises, with the caller’s environment in the environment slot. A formal that has no actual argument but has a default gets a promise whose expression is the default and whose environment is the new function frame (R-ints: Argument evaluation).
-
In the dots. Each argument that matches
...is also a promise, kept in the...object until it is used. -
By hand, with
delayedAssign(). The Language Definition notes that this function makes a promise out of an expression. -
When a package is loaded. Lazy loading fills the namespace with promises that read the real object from a database on first use; the internals manual describes it as populating the environment with promises (R-ints: Lazy loading).
Calls to primitive functions behave differently. The arguments of a builtin such as + are evaluated before the function runs,
and those of a special such as quote or missing are handed over unevaluated and the function decides what to do with them
(R-ints: Argument evaluation). The promises in this page are those of closures, which is every function written in R.
R code cannot see a promise as an object. The manual is explicit: there is generally no way in R code to check whether an object
is a promise, nor to determine the environment of a promise (R-lang: Promise objects). Writing the name of the argument evaluates
it, which is the point. What R does expose is the expression slot, through substitute(), and a way to look at the promises in
an environment without forcing them (shown in Creating and inspecting promises below).
The life cycle of a promise
The diagram follows one promise from its creation to the end of its life. A promise that has been forced is never evaluated again; one whose evaluation was interrupted by an error is evaluated again the next time it is used; a promise that needs its own value while it is being evaluated is an error.
Source: R Internals: The data
The loop on Created is what makes missing(x) and substitute(x) safe to call on an argument that would fail if evaluated:
neither touches the value slot. Discarded is a state of the binding, not of the promise: assigning to a formal argument before
reading it replaces the promise with an ordinary value, and the promise is never evaluated. The first script exercises the three
rules that matter most: a promise is not evaluated at the call, an argument that is never used is never evaluated, and a promise is
evaluated once. It ends with the definition of force().
# A closure's arguments are not evaluated when the function is called.
trace_arg <- function(x) {
cat("the body starts\n")
value <- x
cat("the body has used x\n")
value
}
trace_arg({
cat("the argument is evaluated now\n")
21 * 2
})
#> the body starts
#> the argument is evaluated now
#> the body has used x
#> [1] 42
# An argument that is never used is never evaluated.
ignore <- function(x) "x was never touched"
ignore(stop("this would fail"))
#> [1] "x was never touched"
ignore(message("this message never appears"))
#> [1] "x was never touched"
# A promise is forced at most once: its value is kept.
ticks <- new.env()
ticks$n <- 0L
tick <- function() {
ticks$n <- ticks$n + 1L
ticks$n
}
twice <- function(x) c(x, x)
twice(tick())
#> [1] 1 1
ticks$n
#> [1] 1
# force() is the identity function: its only job is to use the argument.
body(force)
#> x
names(formals(force))
#> [1] "x"
identical(body(force), quote(x))
#> [1] TRUE
Source: R documentation: force
trace_arg() prints "the body starts" before "the argument is evaluated now": the cat() inside the braces of the argument ran
only when the body executed value <- x. ignore(stop("this would fail")) returns normally, and the message() in the second call
never prints. In twice(tick()) the argument appears twice in the body but tick() ran once, which ticks$n confirms. Finally,
force is nothing but function(x) x: its body is the symbol x, and the help page says so too, calling it "semantic sugar" for
just evaluating the symbol.
A promise keeps its environment alive until it is forced
Because the environment slot points at the frame where the expression has to be evaluated, a promise that has not been forced keeps
that whole frame, with everything in it, from being garbage-collected. Forcing the promise sets the slot to NULL, which releases the
frame. The script registers a finalizer on the frame of build(); it runs only after the closure that holds the promise has used it.
# An unforced promise keeps the environment it must be evaluated in alive;
# forcing it lets the environment go (R-ints: the environment is set to NULL).
make_adder <- function(n) {
function(x) x + n
}
build <- function() {
frame <- environment()
reg.finalizer(frame, \(e) cat("the frame of build() was collected\n"))
size <- 1000L
make_adder(size * 2L)
}
adder <- build()
invisible(gc())
cat("collected? not yet: the promise still needs that frame\n")
#> collected? not yet: the promise still needs that frame
adder(1)
#> [1] 2001
invisible(gc())
#> the frame of build() was collected
cat("after forcing the promise, the frame can go\n")
#> after forcing the promise, the frame can go
Source: R Internals: The data
The first gc() finds the frame of build() still reachable, because the promise for size * 2L inside adder needs it. Calling
adder(1) forces the promise, and the next gc() collects the frame. This is rarely a problem, but it explains why long-lived
closures that capture a large local object through an unforced promise can use more memory than expected, and it is one more
reason to force() an argument that a closure keeps (see below). The finalizer message appears before the last cat() because
gc() ran the finalizer synchronously. The
Memory and Copy Semantics page covers the rest of R’s memory model.
When a promise is forced
A promise is forced when its value is needed. The Language Definition says this usually happens inside internal functions, but that a promise can also be forced by evaluating the promise itself, which is occasionally useful when a default expression depends on the value of another formal argument or other variable in the local environment (R-lang: Argument evaluation). It adds that the expression of a promise can itself contain other promises, whenever an unevaluated argument is passed on to another function, and that forcing the outer promise forces the inner ones as they are evaluated. The easiest way to learn the rule is to try it. The script below gives each operation a function with one argument, passes an argument that records whether it was evaluated and tabulates the result.
# Which operations force an argument? Run each one on an argument that
# records whether it was evaluated.
state <- new.env()
state$forced <- FALSE
probe_arg <- function() {
state$forced <- TRUE
1
}
forces <- function(f) {
state$forced <- FALSE
f(probe_arg())
state$forced
}
generic <- function(x) UseMethod("generic")
generic.default <- function(x) "default method"
checks <- list(
"x" = function(x) x,
"force(x)" = function(x) force(x),
"length(x)" = function(x) length(x),
"y <- x" = function(x) {
y <- x
NULL
},
"x <- 1 (assign, never read)" = function(x) {
x <- 1
NULL
},
"if (FALSE) x" = function(x) if (FALSE) x,
"missing(x)" = function(x) missing(x),
"substitute(x)" = function(x) substitute(x),
"quote(x)" = function(x) quote(x),
"match.call()" = function(x) match.call(),
"sys.call()" = function(x) sys.call(),
"nargs()" = function(x) nargs(),
"exists(\"x\")" = function(x) exists("x"),
"get(\"x\")" = function(x) get("x"),
"pass x to a function that ignores it" = function(x) {
(function(y) "ignored")(x)
},
"pass x to a function that uses it" = function(x) {
(function(y) y)(x)
},
"...length()" = function(...) ...length(),
"...elt(1)" = function(...) ...elt(1L),
"list(...)" = function(...) list(...),
"S3 generic dispatching on x" = function(x) generic(x)
)
data.frame(
operation = names(checks),
forces_the_argument = vapply(checks, forces, logical(1)),
row.names = NULL
)
#> operation forces_the_argument
#> 1 x TRUE
#> 2 force(x) TRUE
#> 3 length(x) TRUE
#> 4 y <- x TRUE
#> 5 x <- 1 (assign, never read) FALSE
#> 6 if (FALSE) x FALSE
#> 7 missing(x) FALSE
#> 8 substitute(x) FALSE
#> 9 quote(x) FALSE
#> 10 match.call() FALSE
#> 11 sys.call() FALSE
#> 12 nargs() FALSE
#> 13 exists("x") FALSE
#> 14 get("x") TRUE
#> 15 pass x to a function that ignores it FALSE
#> 16 pass x to a function that uses it TRUE
#> 17 ...length() FALSE
#> 18 ...elt(1) TRUE
#> 19 list(...) TRUE
#> 20 S3 generic dispatching on x TRUE
The table reads as follows.
| Operation | Forces x? |
Why |
|---|---|---|
|
yes |
each one needs the value of |
|
yes |
a builtin or an element access needs the value; |
a call to an S3 generic that dispatches on |
yes |
|
|
no |
a special primitive that inspects the binding, not the value |
|
no |
they return the expression, the symbol or the call, never the value |
|
no |
they count or look up bindings |
passing |
no |
the inner promise holds the outer one, and nobody forces either |
|
no |
the branch is not taken |
|
no |
the binding is replaced; the promise is dropped without being evaluated |
Source: R Internals: Argument evaluation
Three of those rows deserve a second look.
-
The S3 generic.
generic(x)forcedxalthoughgeneric.default()ignores its argument, because the generic isfunction(x) UseMethod("generic")and dispatch needs the class ofx. Every S3 generic therefore forces its dispatch argument, normally the first, at once. See S3 Classes and Generics. -
Passing on. A promise passed to another function becomes the expression of a new promise. Using the argument in the outer function forces the whole chain; not using it anywhere forces nothing. This is how
missing()can look through several calls (see missing() and promises). -
Assignment before a read. A formal argument is a variable in the function frame. Assigning to it before reading it replaces the promise, so the argument that the caller typed is never evaluated. That is legal, but it hides a bug when the caller’s expression had a side effect that the caller expected.
Supplied arguments and default arguments
The Language Definition separates the two cases carefully: supplied arguments are evaluated in the evaluation frame of the calling function, default arguments are evaluated in the evaluation frame of the function (R-lang: Argument evaluation). Both are promises; only the environment slot differs. The consequences are in the next script.
# A default is a promise in the function's own frame, so it can use a
# variable that the body creates later.
source("scenario.R")
due_label <- function(loan_date, label = paste("due", due)) {
due <- loan_date + loan_days # nolint: object_usage_linter.
label
}
due_label(as.Date("2026-06-01"))
#> [1] "due 2026-06-22"
# A supplied argument is evaluated in the caller's frame; a default in the
# function's frame.
scale <- 100
where <- function(supplied, default = scale) {
scale <- 1
c(supplied = supplied, default = default)
}
where(scale)
#> supplied default
#> 100 1
due_label() has a default, label = paste("due", due), that mentions due, a variable that does not exist when the function is
entered. The body creates due and only then evaluates label, so the default finds it. That would be an error in a language that
evaluates defaults when the function is called. where() shows the two environments side by side: where(scale) is called from
the global environment where scale is 100, so the supplied argument evaluates to 100, but the default scale is evaluated in the
function frame, where the body has set scale <- 1. The supplied argument is evaluated in the caller’s frame, where the function’s own scale is not visible.
The timing of a default matters
A default is evaluated when it is first needed, which may be after the arguments it refers to have changed. The manual’s own
example makes the point with a lone label that forces the default early.
# The manual's example: when the default is first forced matters.
late <- function(x, label = deparse(x)) {
x <- x + 1
label
}
early <- function(x, label = deparse(x)) {
label
x <- x + 1
label
}
late(10)
#> [1] "11"
early(10)
#> [1] "10"
# A default may refer to another argument, whatever the order.
rect <- function(area = width * height, width = 3, height = 2) area
rect()
#> [1] 6
rect(width = 10)
#> [1] 20
# Defaults can be inspected without evaluating them.
formals(rect)$area
#> width * height
class(formals(rect)$area)
#> [1] "call"
late(10) returned "11" because label = deparse(x) was evaluated after x <- x + 1. early(10) returned "10": the line that
contains just label forced the default before x changed, and the second label returned the stored value. The same promise
rule applies when a function modifies an argument that a default depends on, which is a classic source of off-by-one surprises in
code that was not written with it in mind. Three further points from the script:
-
Defaults may refer to each other in either order:
area = width * heightis evaluated after the other two are available. -
formals()returns the defaults as unevaluated language objects.formals(rect)$areais the callwidth * heightand no evaluation happened. -
The default is part of the function’s definition, not a value computed at definition time. Programmers coming from Python, where a default is evaluated once when the function is defined, should note that in R a default is evaluated again at every call that needs it, in a fresh frame, so there is no shared mutable default.
An expensive default that is never needed
The shelf report below has a default for overdue that calls a function which has to scan all the loans. When the caller passes a
table with no loans, the function returns early and the scan never runs; when the caller supplies overdue explicitly, the
default is never evaluated either. The message() inside overdue_count() records every time the work is done.
# An expensive default that is never needed is never computed.
source("scenario.R")
overdue_count <- function(loans, today, days) {
message("counting overdue loans")
end <- loans$return_date
end[is.na(end)] <- today
sum(end - loans$loan_date > days, na.rm = TRUE)
}
report <- function(loans,
today = shelf_today, # nolint: object_usage_linter.
days = loan_days, # nolint: object_usage_linter.
overdue = overdue_count(loans, today, days)) {
if (nrow(loans) == 0L) {
return("no loans to report")
}
paste(nrow(loans), "loans,", overdue, "overdue")
}
report(loans[0L, ])
#> [1] "no loans to report"
report(loans)
#> counting overdue loans
#> [1] "60 loans, 32 overdue"
report(loans, days = 30L)
#> counting overdue loans
#> [1] "60 loans, 12 overdue"
report(loans, overdue = 0L)
#> [1] "60 loans, 0 overdue"
report(loans[0L, ]) returned without a message: the promise for overdue was created and never forced. The next call forced it
once, and days = 30L changes what the default sees, because the default refers to the days argument of the same call. The
last call supplied the value, so no scan happened. This is the practical argument for lazy defaults: an optional computation can
be described where the argument is declared and paid for only by the callers that need it. The shelf constants shelf_today and
loan_days are defaults in the same way, which is why no caller has to pass them. The # nolint comments are there because
lintr cannot see the objects that source() creates.
A default that refers to itself
If forcing a promise needs the value of the same promise, R detects it and stops. The message names both usual causes. A default that mentions its own argument, or two defaults that refer to each other, produce it, and so does an earlier error that left the promise in the "being evaluated" state.
# The real error text of a default that refers to itself, and of a pair
# of defaults that refer to each other.
message_of <- function(expr) {
tryCatch(expr, error = function(e) conditionMessage(e))
}
self_ref <- function(x = x) x
message_of(self_ref())
#> [1] "promise already under evaluation: recursive default argument reference or earlier problems?"
cycle <- function(a = b, b = a) a
message_of(cycle())
#> [1] "promise already under evaluation: recursive default argument reference or earlier problems?"
# Supplying the argument avoids the cycle.
cycle(a = 1)
#> [1] 1
self_ref(2)
#> [1] 2
Supplying either argument breaks the cycle. The text, as the interpreter prints it, is
promise already under evaluation: recursive default argument reference or earlier problems?. A default of
x = x is a mistake in R, not a way to say "use the variable x from the caller": the default is evaluated in the function
frame, where x is the argument itself. To use the caller’s x, give the argument another name, or write the default as get("x", envir = parent.frame()), which
looks the variable up in the caller’s frame when the default is first used.
force(): evaluate an argument now
force(x) forces the promise of the formal argument it is given. Its help page states the purpose: it is useful if the argument
will be captured in a closure by the lexical scoping rules and will later be altered by an explicit assignment or an implicit
assignment in a loop or an apply function. The same page says that it is "semantic sugar", that just evaluating the symbol does
the same thing, and that it does not force other promises (it forces the promise that was created when the actual arguments were
matched to the formal arguments)
(R documentation: force). Three consequences:
-
force(x)and a barexon its own line are equivalent.force(x)states the intent, so linters, reviewers and the next reader do not mistake the bare symbol for a leftover and delete it. Prefer it. -
Forcing is not copying.
force(x)evaluates the promise and keeps the value; ifxis later modified, R copies on modification as usual (Memory and Copy Semantics). -
The earliest line of a function is the right place to force an argument that must be evaluated before anything else changes or that you want validated early. An error in the caller’s expression is then raised at the top of the function and not at some distant line, which also makes the traceback shorter.
The function-factory trap
A function factory is a function that returns a function. The returned closure looks up its variables in the environment of the call that created it, and those variables include the factory’s arguments. An argument that was never forced is still a promise there, and the promise reads the caller’s variable when it is finally forced, not when the factory was called. If the caller has changed that variable in between, the closure sees the new value. The first script shows it without a loop.
# A closure that captures an argument captures a promise, not a value.
make_greeter <- function(greeting) {
function(name) paste(greeting, name)
}
greeting <- "Hello"
hello <- make_greeter(greeting)
greeting <- "Goodbye"
hello("Ana")
#> [1] "Goodbye Ana"
# force() evaluates the promise while "Hello" is still its value.
make_greeter_forced <- function(greeting) {
force(greeting)
function(name) paste(greeting, name)
}
greeting <- "Hello"
hello <- make_greeter_forced(greeting)
greeting <- "Goodbye"
hello("Ana")
#> [1] "Hello Ana"
Source: R documentation: force
The first closure printed "Goodbye Ana": its greeting is a promise for the expression greeting, evaluated in the global
environment on the first call of hello(), by which time the global greeting had been reassigned. In the second version
force(greeting) evaluated the promise inside the factory, so the closure holds the string "Hello" and the later assignment cannot
reach it.
Closures made in a loop
The same effect is much harder to spot when the changing variable is a loop counter. The help page of force() has the canonical
example: a for loop builds closures with a factory that does not force its argument, and every closure then sees the last value of
the loop variable. The next blocks build the same closures in two ways, with the version that has the problem second.
# Since R 3.2.0 lapply() and the other higher-order functions force the
# arguments they pass on, so this works without force().
make_adder <- function(n) {
function(x) x + n
}
adders <- lapply(1:3, make_adder)
vapply(adders, \(add) add(10), numeric(1))
#> [1] 11 12 13
adders_map <- Map(make_adder, 1:3)
vapply(adders_map, \(add) add(10), numeric(1))
#> [1] 11 12 13
# A for loop does not force anything: every closure sees the last i.
adders <- list()
for (i in 1:3) {
adders[[i]] <- make_adder(i)
}
vapply(adders, \(add) add(10), numeric(1))
#> [1] 13 13 13
Source: R documentation: lapply
lapply(1:3, make_adder) is safe, and so is Map(make_adder, 1:3): each closure was created with the element it was meant to
capture, and the results are 11, 12 and 13. The for loop version gives 13, 13 and 13. Every promise has the expression i and
the environment of the loop, here the global environment; none of them is forced until the closures are called, after the loop has
finished with i equal to 3. The R FAQ notes that, unlike S, the index variable of a for loop in R lives in the environment
where the loop runs and is not local to the loop (R FAQ 3.3.3: Others), which is why i is still there to read afterwards.
The apply family is safe because of a change in R 3.2.0. The NEWS entry says that higher-order functions such as the apply
functions and Reduce() now force arguments to the functions they apply, to eliminate undesirable interactions between lazy
evaluation and variable capture in closures
(R NEWS: Changes in R 3.2.0). The interaction it describes is the one the
loop version above shows. A for loop is a language construct, not a function, so nothing forces the loop variable, and the
trap is still there in R 4.6.1.
The two fixes
Both fixes make the value be taken at the right time. force() in the factory is the one to prefer, since it fixes every caller
at once. local() evaluates a block in a new environment, so each iteration gets its own n, and the closure is built from that
copy.
# The fix: force() in the factory.
make_adder_forced <- function(n) {
force(n)
function(x) x + n
}
adders <- list()
for (i in 1:3) {
adders[[i]] <- make_adder_forced(i)
}
vapply(adders, \(add) add(10), numeric(1))
#> [1] 11 12 13
# Another fix that needs no change to the factory: local().
adders <- list()
for (i in 1:3) {
adders[[i]] <- local({
n <- i
make_adder(n)
})
}
vapply(adders, \(add) add(10), numeric(1))
#> [1] 11 12 13
Both versions print 11, 12 and 13. Writing force() on every argument of every function would be noise: it is needed when a
closure keeps an argument that can change before the closure runs. A function that only uses its arguments while it executes does
not need it. Replacing the loop with lapply(), vapply() or Map() is the other way out, and it is what most modern code does.
The same trap on the shelf
The loan checkers below test whether a loan lasted longer than a limit of 7, 14 or 21 days. The factory is called in a loop over the limits and the result is stored under the limit’s name.
# The same trap with the shelf: one checker per loan limit, built in a loop.
source("scenario.R")
make_checker <- function(limit) {
function(days) days > limit
}
loan_length <- as.integer(loans$return_date - loans$loan_date)
limits <- c(7L, 14L, 21L)
checkers <- list()
for (limit in limits) {
checkers[[as.character(limit)]] <- make_checker(limit)
}
vapply(checkers, \(chk) sum(chk(loan_length), na.rm = TRUE), integer(1))
#> 7 14 21
#> 25 25 25
# With force() each checker keeps its own limit.
make_checker <- function(limit) {
force(limit)
function(days) days > limit
}
checkers <- list()
for (limit in limits) {
checkers[[as.character(limit)]] <- make_checker(limit)
}
vapply(checkers, \(chk) sum(chk(loan_length), na.rm = TRUE), integer(1))
#> 7 14 21
#> 47 38 25
# lapply() over the limits needs no force().
checkers <- lapply(limits, make_checker)
names(checkers) <- limits
vapply(checkers, \(chk) sum(chk(loan_length), na.rm = TRUE), integer(1))
#> 7 14 21
#> 47 38 25
Source: R documentation: force
With the unforced factory every checker uses the last limit, so the counts for 7, 14 and 21 days are all 25, the number of returned
loans longer than 21 days. After force(limit) the counts differ (47, 38 and 25), and lapply(limits, make_checker) gives the same
numbers without any forcing, as the 3.2.0 change promises. A wrong answer that looks plausible, 25 three times, is typical of this
bug: nothing fails, and the code has to be read twice to see why.
Creating and inspecting promises
delayedAssign(x, value, eval.env = parent.frame(1), assign.env = parent.frame(1)) creates a promise to evaluate value when
the variable x is first accessed. The help page describes it as direct access to the lazy evaluation mechanism used for
interpreted functions. The expression is evaluated in eval.env, whose contents may have changed in the meantime, and the
promise is stored in assign.env. After the first access the value is fixed and the expression is not evaluated again, though the
promise keeps its expression
(R documentation: delayedAssign). Both environments default to
the currently active one, so inside a function the promise lives in the function’s frame.
# delayedAssign() binds a name to a promise that you build by hand.
delayedAssign("book_count", {
cat("reading books.csv\n")
nrow(read.csv("books.csv"))
})
cat("the promise exists, nothing has been read yet\n")
#> the promise exists, nothing has been read yet
exists("book_count", inherits = FALSE)
#> [1] TRUE
book_count
#> reading books.csv
#> [1] 16
book_count
#> [1] 16
# The expression is evaluated in eval.env and the promise is bound in
# assign.env; both default to the calling frame.
eval_env <- new.env()
assign_env <- new.env()
assign("bonus", 5, envir = eval_env)
delayedAssign("answer", bonus * 2, eval.env = eval_env, assign.env = assign_env)
exists("answer", envir = assign_env, inherits = FALSE)
#> [1] TRUE
exists("answer", inherits = FALSE)
#> [1] FALSE
get("answer", envir = assign_env)
#> [1] 10
# The expression may refer to something that does not exist yet.
delayedAssign("later", defined_after)
defined_after <- "now it exists"
later
#> [1] "now it exists"
# Inside a function the default is the function's own frame.
lazy_local <- function() {
delayedAssign("value", {
cat("evaluated on first use\n")
"ready"
})
cat("before use\n")
c(value, value)
}
lazy_local()
#> before use
#> evaluated on first use
#> [1] "ready" "ready"
Source: R documentation: delayedAssign
The file books.csv was not read until book_count was first used, and the second access did not read it again. exists() is a
way to ask whether the name is bound without evaluating the promise. The second block uses two environments: the expression
bonus * 2 is evaluated in eval_env, where bonus is 5, and the promise is stored in assign_env, so
exists("answer", inherits = FALSE) in the global environment is FALSE while get("answer", envir = assign_env) is 10. The
third block shows that value is captured unevaluated: defined_after does not exist at the time of the delayedAssign() call
and nothing fails, because nothing is evaluated until later is used.
Where promises are used on purpose
-
Lazy loading of packages. The code and, optionally, the data of an installed package are stored in a database, and loading the namespace populates it with promises that fetch the real object the first time it is used. R-ints says lazy loading is always used for the code of a package and optional for its datasets, which the package maintainer selects with
LazyDatainDESCRIPTION; it keeps loading a namespace cheap, because nothing is read until it is used (R-ints: Lazy loading). See Packages, Libraries and Namespaces and Creating Packages. -
Expensive setup that may not be needed. A script can
delayedAssign()a connection, a data load or a model, and the code that never touches the name pays nothing. -
Not a replacement for an active binding. A promise is evaluated once.
makeActiveBinding()(documented withlockBinding()in R documentation: bindenv) calls a function on every access, which is the right tool for a value that must be fresh each time. The block below shows the difference: the active binding prints its message on every access, the promise only on the first.
# A promise is evaluated once; an active binding runs on every access.
delayedAssign("once", {
cat("promise evaluated\n")
"first value"
})
makeActiveBinding("every_time", function() {
cat("active binding called\n")
"fresh value"
}, environment())
once
#> promise evaluated
#> [1] "first value"
once
#> [1] "first value"
every_time
#> active binding called
#> [1] "fresh value"
every_time
#> active binding called
#> [1] "fresh value"
Source: R documentation: bindenv
Looking at promises without forcing them
substitute(x, env) returns the expression slot of the promise bound to x in env, without evaluating it. That is how the help
page of delayedAssign() shows how to list the promises of an environment, and it works on the environment of a function call.
Anything that evaluates the bindings instead, such as as.list(env), forces them.
# Look at the promises in a frame without forcing them: substitute() with an
# environment returns the expression slot.
source("scenario.R")
frame <- (function(x, y = 1, z) environment())(
loan_days * 2L, "y", {
cat("evaluated\n")
pi + 2
}
)
sort(names(frame))
#> [1] "x" "y" "z"
substitute(z, frame)
#> {
#> cat("evaluated\n")
#> pi + 2
#> }
substitute(y, frame)
#> [1] "y"
substitute(x, frame)
#> loan_days * 2L
# as.list() on the environment forces every promise.
values <- as.list(frame)
#> evaluated
values$z
#> [1] 5.141593
values$x
#> [1] 42
# The expression is still there after forcing.
substitute(z, frame)
#> {
#> cat("evaluated\n")
#> pi + 2
#> }
# delayedAssign() leaves the expression in the promise too (assign.env must
# not be the global environment for substitute() to show it).
msg <- "old"
env <- new.env()
delayedAssign("shown", msg, assign.env = env)
msg <- "new"
substitute(shown, env)
#> msg
get("shown", envir = env)
#> [1] "new"
Source: R documentation: substitute
substitute(z, frame) returned the braced block that the caller typed and did not print "evaluated"; as.list(frame) printed
it, because it forced the three promises. After forcing, substitute(z, frame) still returns the same expression, since the
value is stored next to the expression and does not replace it. A small caveat from the help page: substitute() leaves the
symbol alone when env is the global environment, so a promise made by delayedAssign() in the global environment cannot be
inspected this way, and the script binds the last one in a fresh environment. Packages such as rlang offer quosures, which turn
an argument into an ordinary object holding the expression and the environment, and lobstr’s sxp() prints the internal
structure of an object. Both are community packages, are not used here and are mentioned only because tutorials point to them.
missing() and promises
missing(x) is a special primitive that tells whether a formal argument was supplied by the caller. It must not evaluate its
argument, and it can be used only in the immediate body of the function that defines the argument, not in a nested function or in
a local() call
(R documentation: missing). The internals manual explains how it
works: the matching of actual to formal arguments sets a "missing" bit on arguments that were not given, and missing() also
follows promises, because missingness can be passed on from function to function (using a formal as an actual argument does not
count as evaluation)
(R-ints: Missingness).
# missing() is TRUE for an argument the caller did not supply, even when it
# has a default.
f <- function(x = 5) {
cat("missing:", missing(x), " value:", x, "\n")
}
f()
#> missing: TRUE value: 5
f(5)
#> missing: FALSE value: 5
# Assigning to the argument makes it not missing.
g <- function(x) {
cat("before the assignment:", missing(x), "\n")
x <- 1
cat("after the assignment: ", missing(x), "\n")
}
g()
#> before the assignment: TRUE
#> after the assignment: FALSE
# Missingness is passed from function to function through the promise.
inner <- function(y) missing(y)
outer <- function(x) inner(x)
outer()
#> [1] TRUE
outer(1)
#> [1] FALSE
# ... but using the value is an error, and the inner default is not used.
inner_default <- function(y = "inner default") y
outer_default <- function(x) inner_default(x)
tryCatch(
outer_default(),
error = function(e) conditionMessage(e)
)
#> [1] "argument \"x\" is missing, with no default"
# missing() works only on arguments of the running function.
tryCatch(
missing(not_an_argument),
error = function(e) conditionMessage(e)
)
#> [1] "'missing(not_an_argument)' did not find an argument"
Source: R documentation: missing
The first block shows the rule that surprises people who treat a default as a value: with a default, missing(x) is TRUE when the
caller did not supply x, even though x itself has a perfectly good value (5). missing() tells you how the argument got its
value, not whether it has one. The second block shows the other end: the first assignment to the argument clears the missing bit,
and after it missing(x) is FALSE. The help page says missing(x) is only reliable if x has not been altered since entering the function, and gives the example of
x <- match.arg(x), after which missing(x) is always false.
The third block follows missingness through a call. outer() has no argument, yet inner(x) receives a promise whose expression is
the symbol x, which is missing in outer(); missing(y) in inner() follows that promise and returns TRUE. Using the value is
another matter: the error argument "x" is missing, with no default names the argument of the outermost function that lacks it, and
the inner function’s default "inner default" is not used, since the promise exists and has to be evaluated. The last block shows
the error that missing() raises when its argument is not a formal of the running function.
Using missing() in a function
# The shelf: a loan that has no return date is still out.
source("scenario.R")
loan_status <- function(loan_date,
return_date,
today = shelf_today) { # nolint: object_usage_linter.
if (missing(return_date)) {
paste("out for", as.integer(today - loan_date), "days")
} else {
paste("returned after", as.integer(return_date - loan_date), "days")
}
}
loan_status(as.Date("2026-06-01"))
#> [1] "out for 29 days"
loan_status(as.Date("2026-06-01"), as.Date("2026-06-15"))
#> [1] "returned after 14 days"
# missing() is only reliable until the argument is altered: it is always
# FALSE after x <- match.arg(x).
pick <- function(order = c("title", "member")) {
before <- missing(order)
order <- match.arg(order)
c(before = before, after = missing(order))
}
pick()
#> before after
#> TRUE FALSE
Source: R documentation: missing
loan_status() uses missing() for its optional return_date instead of a sentinel value. The last block checks the warning from the
help page: pick() finds missing(order) true before match.arg() and false afterwards, so a function that needs both facts
must record missing() first. Prefer a default of NULL and is.null() when callers are expected to pass the "absent" value
along from variables, and keep missing() for the cases where the caller either writes the argument or does not. The
Functions page compares the idioms.
Consequences for substitute()
substitute() is the other half of the promise: it returns the expression the caller typed, which is the basis of
labels, deparse(substitute(x)) and non-standard evaluation. The help page says that when a symbol is bound to a promise in the
environment, the expression slot of the promise replaces the symbol, whether the promise is a formal argument or was created by
delayedAssign(); an ordinary variable is replaced by its value, unless the environment is the global one
(R documentation: substitute).
# substitute() reads the expression slot of the promise.
label <- function(x) deparse(substitute(x))
label(shelf_today + 7)
#> [1] "shelf_today + 7"
label(1:3 * 2)
#> [1] "1:3 * 2"
# The expression survives forcing: the value is stored next to it.
forced <- function(x) {
value <- x
list(expression = substitute(x), value = value)
}
forced(6 * 7)
#> $expression
#> 6 * 7
#>
#> $value
#> [1] 42
#>
# For an argument that was not supplied, substitute() returns the default.
with_default <- function(x = 1 + 1) substitute(x)
with_default()
#> 1 + 1
with_default(2 + 2)
#> 2 + 2
# A promise can wrap another promise: x is passed on unevaluated.
relay <- function(y) label(y)
relay(a + b)
#> [1] "y"
# Typical use: label a result by what the caller typed.
describe_count <- function(x) {
paste(deparse(substitute(x)), "has", length(x), "elements")
}
numbers <- c(4, 8, 15, 16, 23, 42)
describe_count(numbers)
#> [1] "numbers has 6 elements"
describe_count(numbers[numbers > 10])
#> [1] "numbers[numbers > 10] has 4 elements"
# quote() quotes what you type; it never sees the promise.
quote_it <- function(x) quote(x)
quote_it(1 + 1)
#> x
Source: R documentation: substitute
label(shelf_today + 7) printed the text of the expression, although shelf_today does not exist in that script: the expression
was never evaluated. forced() shows that the expression is available after the value has been computed. For an argument that
was not supplied, the promise holds the default expression, so with_default() returns 1 + 1. relay(a + b) returns y, not
a + b: relay() calls label(y), so the promise for the argument of label() has the expression y, and that is what
substitute() returns. Each function in a chain sees only the expression its own caller wrote. quote_it() returns the symbol x, which is what quote()
always does: it never sees the promise, because it quotes what is written.
The last example of the page on substitute() is the one in the help page of substitute(), with different names.
# substitute() inside a function sees promises, not values; the result
# depends on whether the promise was already forced and on assignments.
as_value <- function(x, y = x) {
x <- x + 1
y
}
as_expression <- function(x, y = substitute(x)) {
x <- x + 1
y
}
as_symbol <- function(x, y) {
if (missing(y)) {
y <- substitute(x)
}
x <- x + 1
y
}
count <- 10
as_value(count)
#> [1] 11
as_expression(count)
#> [1] 11
as_symbol(count)
#> count
typeof(as_symbol(count))
#> [1] "symbol"
Source: R documentation: substitute
All three functions try to hand back "what the caller wrote". as_value() returns 11: the default y = x is evaluated after
x <- x + 1, so it sees the new value. as_expression() returns 11 too, because by the time the default substitute(x) runs,
x is no longer a promise but an ordinary variable, and substitute() replaces it with its value. Only as_symbol(), which calls
substitute(x) before the assignment, returns the symbol count. This is what the R FAQ means by "equivalent to S, until
assignment takes place". Capture expressions at the top of the function, before touching the arguments.
Language Objects and Quoting builds on this with quote(), bquote(),
deparse() and match.call(), and
Evaluation and Non-Standard Evaluation with eval()
in a chosen environment; both are only previewed here.
Side effects and the order of evaluation
The manual’s advice is direct: do not use arguments to cause side effects. If a function never uses an argument, the argument is never evaluated and the side effect does not happen; if it uses it twice, the effect still happens once; and the order in which the arguments are evaluated is the order of use in the body, which can be different from the order in the call.
# Side effects in an argument happen when, and if, the argument is used.
noisy <- function(label) {
message("evaluating ", label)
label
}
use_second <- function(first, second) second
use_second(noisy("first"), noisy("second"))
#> evaluating second
#> [1] "second"
# Order of evaluation follows the use in the body, not the argument list.
combine <- function(a, b) {
cat("body starts\n")
b_value <- b
a_value <- a
paste(a_value, b_value)
}
combine(noisy("a"), noisy("b"))
#> body starts
#> evaluating b
#> evaluating a
#> [1] "a b"
In the first call noisy("first") never ran, so only "evaluating second" appears. In the second, combine() reads b before a,
and the messages come in the order b, a, although a is written first. A reader who assumes that arguments are evaluated from left
to right will be wrong for any closure that uses them in another order. For the same reason, argument expressions with side
effects, such as reading from a connection or advancing a counter, make the program’s behaviour depend on the function’s body.
# An assignment in an argument runs in the caller's frame, when it is forced.
unused <- function(x) "not used"
used <- function(x) x
n <- 1
unused(n <- 99)
#> [1] "not used"
n
#> [1] 1
used(n <- 99)
n
#> [1] 99
print(used(n <- 100))
#> [1] 100
n
#> [1] 100
# R-lang: there is no guarantee that the argument is ever evaluated.
store_and_report <- function(value) {
if (FALSE) value
"done"
}
store_and_report(stored <- 1)
#> [1] "done"
exists("stored")
#> [1] FALSE
The manual points to the C idiom foo(x = y), which assigns as well as passes, and warns that the same style should not be used in
R. The assignment n <- 99 as an argument ran in the caller’s frame when, and only when, the argument was forced: unused(n <- 99)
left n at 1 and used(n <- 99) set it to 99. The script also shows a quirk worth knowing. used(n <- 99) printed nothing at
the console, although used() ends with x: the value of an assignment is invisible, and, as observed on R 4.6.1, evaluating a
promise whose expression is an assignment leaves that visibility flag off. print(used(n <- 100)) shows the value. If you see a function return its argument
invisibly, an assignment in the call is the first place to look. The last call, store_and_report(stored <- 1), confirms that
stored was never created.
Arguments in the dots
The arguments that match ... are promises too. ...length() counts them without evaluating any, ...elt(n) forces only
the nth, list(...) forces all of them from left to right, and substitute() applied to the dots recovers every expression.
The Language Definition notes that capturing the dots as a list forces the promises.
# The arguments in ... are promises as well; each one is forced only when it
# is used.
source("scenario.R")
noisy <- function(label) {
message("evaluating ", label)
label
}
second_only <- function(...) {
cat("how many:", ...length(), "\n")
...elt(2L)
}
second_only(noisy("first"), noisy("second"), noisy("third"))
#> how many: 3
#> evaluating second
#> [1] "second"
# list(...) forces them all, left to right.
all_of_them <- function(...) {
length(list(...))
}
all_of_them(noisy("a"), noisy("b"))
#> evaluating a
#> evaluating b
#> [1] 2
# substitute() on ... recovers every expression without forcing any.
expressions <- function(...) {
vapply(substitute(list(...))[-1L], deparse, character(1L))
}
expressions(shelf_today + 7, nrow(books), "text")
#> [1] "shelf_today + 7" "nrow(books)" "\"text\""
# Asking for an element that was not supplied is an error.
tryCatch(
second_only(noisy("only one")),
error = function(e) conditionMessage(e)
)
#> how many: 1
#> [1] "the ... list contains fewer than 2 elements"
Source: R documentation: dots
second_only() printed "how many: 3" and evaluated only the second argument. all_of_them() evaluated both. The last function
returns the unevaluated source text of each argument, which is the basis of functions that label their inputs, such as data.frame()
using the argument expressions as column names. The last call asks for an element that was not supplied and shows the real error
text; no argument was evaluated on the way.
Guards: stopifnot() and early validation
stopifnot(..., exprs, exprObject, local = TRUE) is a function whose arguments are evaluated lazily and in order. Its help page
says the conceptual equivalent is a sequence of if (any(is.na(A)) || !all(A)) stop(...) statements: the expressions are
evaluated sequentially, and evaluation stops as soon as one is not TRUE. That sequential evaluation has been the behaviour
since R 3.5.0 and stopifnot(exprs = { ... }) has existed since the same version. Names given to the arguments are used in place
of the default error message, which came in R 4.0.0
(R documentation: stopifnot and
R NEWS: Changes in R 4.0.0).
# stopifnot() evaluates its arguments lazily, left to right, and stops at the
# first one that is not TRUE.
message_of <- function(expr) {
tryCatch(expr, error = function(e) conditionMessage(e))
}
message_of(stopifnot(is.numeric("a")))
#> [1] "is.numeric(\"a\") is not TRUE"
message_of(stopifnot(1 == 1, 2 < 1, stop("never evaluated")))
#> [1] "2 < 1 is not TRUE"
# Named arguments become the error message (R 4.0.0).
message_of(stopifnot("the limit must be positive" = -1 > 0))
#> [1] "the limit must be positive"
# exprs = takes a braced block of conditions.
message_of(stopifnot(exprs = {
is.numeric(21L)
21L > 30L
stop("never evaluated")
}))
#> [1] "21L > 30L is not TRUE"
Source: R documentation: stopifnot
The first call fails on is.numeric("a") with the expression text as the message. The second call never reaches
stop("never evaluated"): the third argument is a promise that nobody forced, since evaluation stopped at the failing
2 < 1. A condition can therefore safely depend on an earlier one, as in stopifnot(is.function(f), f(1) > 0), where the second
expression would itself fail if f were not a function. The names in the
third call turn the message into "the limit must be positive". The block form exprs = { ... } takes the whole block unevaluated and
evaluates it one expression at a time with the same stop-at-the-first-failure behaviour.
# A guard at the top of a function forces the arguments it checks, so an
# error in the caller's expression is reported where it is used.
checked_limit <- function(days) {
stopifnot(is.numeric(days), length(days) == 1L, days > 0)
days * 2
}
message_of(checked_limit(-3))
#> [1] "days > 0 is not TRUE"
message_of(checked_limit("21"))
#> [1] "is.numeric(days) is not TRUE"
checked_limit(21)
#> [1] 42
# The call recorded in the condition is the function that forced the promise.
caught <- tryCatch(checked_limit(stop("bad argument")), error = identity)
conditionMessage(caught)
#> [1] "bad argument"
conditionCall(caught)
#> stopifnot(is.numeric(days), length(days) == 1L, days > 0)
# A wrapper has to hand the unevaluated block to stopifnot(): the manual's
# assert() does it with substitute() and eval.parent().
assert <- function(exprs) {
eval.parent(substitute(stopifnot(exprs = exprs)))
}
message_of(assert({
is.numeric(21L)
21L > 30L
stop("never evaluated")
}))
#> [1] "21L > 30L is not TRUE"
Source: R documentation: stopifnot
A guard at the top of a function forces the arguments it mentions, in order, and reports the first condition that failed. The
last two lines show where an error in the caller’s expression is reported. checked_limit(stop("bad argument")) fails while
stopifnot() is forcing days, so the condition’s call is the stopifnot(...) call that was running at the time, not the
checked_limit() call and not the stop() the caller wrote. A promise is evaluated in the caller’s environment, but the
call stack that the error sees is that of the code that forced it. The help page warns against wrapping exprs = in a helper that
forwards its argument as stopifnot(exprs = exprs), which would evaluate all parts and pass on only the last result; the assert()
in the script uses eval.parent(substitute(...)), the form that the help page recommends, and keeps the one-at-a-time behaviour.
Input validation as a design topic continues on
Defensive Programming and Input Validation.
Defaults that depend on the clock, match.arg() and on.exit()
A default of Sys.time()
Sys.time() in a default is evaluated at the first use of the argument, not when the function is called. A function that does a
long piece of work before it uses the default records a later time than the call time. The script compares the returned time with
the time before the call, which makes the check independent of the wall clock; both comparisons print TRUE.
# Sys.time() in a default is evaluated at first use, not at the call.
started <- function(when = Sys.time()) {
Sys.sleep(0.2)
when
}
before <- Sys.time()
as.numeric(difftime(started(), before, units = "secs")) > 0.15
#> [1] TRUE
# force() it first to pin the time of the call.
called <- function(when = Sys.time()) {
force(when)
Sys.sleep(0.2)
when
}
before <- Sys.time()
as.numeric(difftime(called(), before, units = "secs")) < 0.15
#> [1] TRUE
Source: R documentation: Sys.time
started() slept 0.2 seconds before using when, so the time it returned was at least 0.15 seconds after the call. called() forced
when first and returned a time within 0.15 seconds of the call. The same applies to Sys.Date(): a default of Sys.Date()
in a function that runs across midnight can return the next day. For results that must be reproducible, take the date as an
argument and pass it on, as the shelf scenario does with shelf_today.
match.arg() with a default of several choices
The idiom type = c("a", "b") for a choice argument relies on laziness. The default is the vector of valid values. It is a
promise like any other, and match.arg(order) finds the choices by looking at the formal arguments of the calling function
(R documentation: match.arg). When the caller supplies nothing, the
argument is identical to the full set and match.arg() returns the first element; when the caller supplies a value it is
matched against the choices, with partial matching.
# match.arg() with a lazy default: the default is the vector of choices.
loan_report <- function(order = c("title", "member", "date")) {
order <- match.arg(order)
paste("sorted by", order)
}
loan_report()
#> [1] "sorted by title"
loan_report("mem")
#> [1] "sorted by member"
tryCatch(loan_report("year"), error = function(e) conditionMessage(e))
#> [1] "'arg' should be one of “title”, “member”, “date”"
formals(loan_report)$order
#> c("title", "member", "date")
Source: R documentation: match.arg
loan_report() returned the first choice, matched "mem" to "member", and, for "year", failed with the message
'arg' should be one of “title”, “member”, “date”. The quotation marks in that message depend on the locale: a UTF-8
session prints typographic quotes, and other locales print plain ones. formals(loan_report)$order shows the default
as the unevaluated call c("title", "member", "date"). As the missing() section showed, missing(order) is false after
order <- match.arg(order). R 4.6.0 added a several.ok = "all" option to match.arg(); see its help page (R NEWS: Changes
in R 4.6.0).
on.exit() and promises
on.exit(expr, add = FALSE, after = TRUE) is a special primitive that records its first argument without evaluating it and runs it
in the function’s frame when the function exits, normally or through an error. Because it is evaluated at the end, it sees
the state of the function at that moment, and if it mentions an argument that has not been forced it forces the promise at that
point.
# on.exit() expressions run when the function ends: they may force a promise.
clean_up <- function(x) {
on.exit(cat("on exit the value is", x, "\n"))
cat("body\n")
invisible(NULL)
}
clean_up({
cat("argument evaluated\n")
1
})
#> body
#> argument evaluated
#> on exit the value is 1
# The expression is recorded unevaluated and runs in the function's frame
# when the function exits, so it sees later changes to local variables.
leave <- function() {
status <- "started"
on.exit(cat("status at exit:", status, "\n"))
status <- "finished"
invisible(NULL)
}
leave()
#> status at exit: finished
Source: R documentation: on.exit
In the first call the argument was evaluated only when the exit code ran: "body" is printed first, then "argument evaluated", then
the exit message. If the exit code is the only place where a promise is used and the expression fails, the error is raised
during the exit handling. The second function shows that the recorded expression sees "finished", the value at exit, not
"started". The help page recommends substitute() when the value at the time of the call is wanted. With add = TRUE several
expressions are run in order, and after = FALSE reverses it, which is useful for releasing resources in the opposite order to
their acquisition. R 4.0.2 fixed the matching of named arguments in on.exit() calls (R NEWS: Changes in R 4.0.2).
Errors, tryCatch() and promises
An error in a promise is signalled when the promise is forced, by the function that forces it. A call that never forces the argument
cannot fail because of it, and the function that does force it is the one that appears in the condition’s call.
tryCatch(expr, error = ...) is an ordinary closure: expr is a promise that it forces inside its own handler setup, which is how
it can catch an error raised by the code you wrote as the argument
(R documentation: conditions).
# An error in a promise is signalled when the promise is forced, in the
# function that forces it.
lazy_use <- function(x) {
"never touched"
}
tryCatch(lazy_use(stop("no error")), error = function(e) "caught")
#> [1] "never touched"
forces_late <- function(x) {
cat("about to use x\n")
x
}
res <- tryCatch(forces_late(stop("failure in the argument")),
error = function(e) e
)
#> about to use x
conditionMessage(res)
#> [1] "failure in the argument"
deparse(conditionCall(res))
#> [1] "forces_late(stop(\"failure in the argument\"))"
# try() and tryCatch() are ordinary functions: their expr argument is a
# promise that they force inside the handler.
try_or <- function(expr, default) {
tryCatch(expr, error = function(e) default)
}
try_or(sqrt("a"), NA_real_)
#> [1] NA
try_or(stop("bad"), "fallback")
#> [1] "fallback"
Source: R documentation: conditions
lazy_use(stop("no error")) did not fail. forces_late() printed "about to use x" before the error was raised, and the
condition’s call is forces_late(stop("failure in the argument")): the function that forced the promise. try_or() shows the
pattern in a few lines: any function that wants to run user code under a handler takes it as an argument and forces it where the
handler is in place. Conditions in general are covered on
Conditions and Error Handling.
Forcing a promise twice after an error
A promise whose evaluation is interrupted by an error is not marked as evaluated. If the same argument is used again, R evaluates it
again and emits a warning, restarting interrupted promise evaluation, so that the second attempt is visible. The script catches
that warning with withCallingHandlers() and muffles it so that the script exits cleanly.
# A promise that was interrupted by an error can be forced again, with a
# warning.
retry <- function(x) {
first <- tryCatch(x, error = function(e) "first attempt failed")
second <- withCallingHandlers(
tryCatch(x, error = function(e) "second attempt failed"),
warning = function(w) {
cat("warning:", conditionMessage(w), "\n")
invokeRestart("muffleWarning")
}
)
c(first, second)
}
retry({
cat("evaluating the argument\n")
stop("always fails")
})
#> evaluating the argument
#> warning: restarting interrupted promise evaluation
#> evaluating the argument
#> [1] "first attempt failed" "second attempt failed"
Source: R documentation: conditions
The argument ran twice, as the two "evaluating the argument" lines show, and the warning appeared between them. Without the
handler, a script run with options(warn = 1) would print the warning as it happens. If you see this warning in a log, a
promise has been used a second time after failing the first time, which usually means a try() or tryCatch() wraps a call
whose argument is reused afterwards.
Functions that look like control flow
Promises make it possible to write functions that behave like if or for: they receive code, not values, and control when it
runs. with_banner() below uses on.exit() to print a closing line even if the code fails; repeat_n() shows a mistake that is
easy to make, assuming a promise can be run repeatedly; and repeat_each() shows the way to do it with substitute() and
eval().
# Functions that look like control flow take a promise and decide when, how
# often and where to force it.
with_banner <- function(expr) {
cat("[begin]\n")
on.exit(cat("[end]\n"))
expr
}
with_banner(cat("work happens here\n"))
#> [begin]
#> work happens here
#> [end]
repeat_n <- function(n, expr) {
for (i in seq_len(n)) {
expr
}
invisible(NULL)
}
repeat_n(3L, cat("tick\n"))
#> tick
# The loop above ran the body once: a promise is evaluated once. To run an
# expression several times, take its code with substitute() and eval() it.
repeat_each <- function(n, expr) {
code <- substitute(expr)
for (i in seq_len(n)) {
eval(code, parent.frame())
}
invisible(NULL)
}
repeat_each(3L, cat("tock\n"))
#> tock
#> tock
#> tock
# Standard functions built the same way.
suppressWarnings(as.integer("x"))
#> [1] NA
system.time(NULL)[["elapsed"]] >= 0
#> [1] TRUE
with_banner() printed the work between the two banner lines, which means expr was evaluated at the point of its use inside the
function. repeat_n(3L, cat("tick\n")) printed "tick" once: the promise was forced on the first iteration and the later ones used
the stored value. repeat_each() captures the expression with substitute() and evaluates it with eval() in the caller’s
frame (parent.frame()), which runs it every time, but is non-standard evaluation with the risks described on
Evaluation and Non-Standard Evaluation. Standard
functions such as suppressWarnings() and system.time() are written in the first style, and their arguments are promises that
are forced inside the wrapper.
Common errors and warnings
| Message | When it appears | What to do |
|---|---|---|
|
A default refers to its own argument ( |
Rename the variable the default refers to, or supply the argument; look at the first error in the session |
|
An argument whose evaluation failed is used again, for example after being caught with |
Evaluate the argument once, catch the error where it is first used, and do not reuse the failed argument |
|
The body evaluates a formal argument that the caller did not supply and that has no default, directly or through a chain of calls |
Test with |
|
|
Call |
|
|
Check |
|
|
Use one of the listed values; the quotation marks depend on the locale |
|
A |
Fix the argument, or give the condition a name to get your own message |
Source: R Internals: Missingness
Every message in the table was produced by a script of this page.
Where older tutorials differ
Material about lazy evaluation is among the oldest in the R literature, and several well-known statements are now out of date or were never general.
| Older tutorial | Current practice | Why | Version |
|---|---|---|---|
“ |
|
The apply functions and |
R 3.2.0 |
“Put |
Force an argument that a closure keeps and that can change before the closure runs, or that must be evaluated early; do not decorate every function |
A promise that is only used while the function runs does not need forcing |
always |
“Default arguments are evaluated when the function is defined” (the Python rule) |
A default is evaluated in the function’s frame when first needed, at every call |
R-lang: Argument evaluation |
always |
“ |
It tells you whether the caller supplied |
|
always |
“Check arguments with |
|
|
R 3.5.0, 4.0.0 |
Tutorials that inspect promises with the |
|
|
2026 |
Older code that avoids naming the arguments of |
Write |
named arguments of |
R 4.0.2 |
“R passes arguments by reference until they are modified” |
R has call-by-value semantics with lazy evaluation: assigning to an argument never changes the caller’s variable |
R-lang: Argument evaluation (the copy is avoided internally by reference counting, which replaced |
always |
Source: R NEWS: Changes in R 3.2.0
Community references on the same topic include the chapter on functions in the book Advanced R (Advanced R: Functions), which has a section on promises and says that inspecting a promise from R code forces it. It is community material and it is not used as a source for any claim above.
See also
-
Functions: argument matching, defaults,
...and replacement functions. -
Scoping, Environments and Closures: where a promise’s environment comes from.
-
Function Factories and Higher-Order Functions: the factory pattern in depth.
-
The apply Family and Functionals: the functions that force their arguments since R 3.2.0.
-
Language Objects and Quoting and Evaluation and Non-Standard Evaluation: what you can do with the expression slot.
-
Conditions and Error Handling and Defensive Programming and Input Validation:
tryCatch(),stopifnot()and checks at function entry. -
Pipes and the Native Pipe: the pipe rewrites a call before evaluation, so its right-hand side is a promise like any other argument.
References
-
The R Language Definition: Promise objects, Argument evaluation
-
R Internals: The data (the promise type), Argument evaluation, Missingness, Lazy loading
-
R FAQ: 3.3.3 Others (the differences between R and S)
-
R NEWS: Changes in R 4.0.0, 4.0.2 and 4.6.0
-
R NEWS (R 3.x): Changes in R 3.2.0