# dot-dot-dots everywhere
args(c)function (...)
NULL
args(list)function (...)
NULL
args(print)function (x, ...)
NULL
function (.data, ..., .by = NULL, .preserve = FALSE)
NULL
function (.x, .f, ..., .progress = FALSE)
NULL
... in RCollin K. Berke, Ph.D.
October 26, 2025
The
...construct can be a slippery thing to get a hold of until you know the trick.
The first step toward improvement is admiting you have a problem. My problem? Understanding the dot-dot-dot (i.e., ...) when it comes to writing functions in R. I had an intuitive sense of how ...s worked, especially when using functions that had these as part of their implementation. I struggled, though, when applying them to my own self-defined functions. A few questions would constantly arise: Am I implementing these correctly? What am I missing? These notes are thus:
...s through drafting some notes.This post is written in the spirit of publishing more frequent blog posts. It’s a bit of a scratchpad of ideas, concepts, and/or ways of working that I found to be useful and interesting. As such, what’s here is lightly edited. Be aware: there will likely be spelling, grammatical, or syntactical errors along with some disjointed, incomplete ideas.
...?In terms of naming conventions, the dot-dot-dot is most commonly referred to as dots, three-dots, or just ....
The Advanced R book by Hadley Wickham contains a concise and useful explanation. To summarise, ...s are a special argument. When applied to a function, that function can take, as inputs, any additional arguments. Dots can also be used to pass arguments to another function. This is referred to as forwarding arguments.
A blog post from Burns Statistics further states ... allows for:
Simplifying further, Josiah Parry has a really good YouTube video overviewing ... fundamentals. The key point made in the video is dots are used to pass arguments (01M38S). In addition, the video explains another interesting expression of the power of dots (03M22S):
it lets us take different objects restructure them into the required format then pass an arguments to another function.
This is known as capturing or collecting arguments passed to dots, a powerful utility. This use of dots will be discussed later in these notes.
I highly suggest reviewing this video. It was a resource about the basics of ... which just clicked for me.
Many R functions implement the ..., including but not limited to those of Base R and the Tidyverse. Here are a few examples of functions that implement ... into their API.
...s used?Let’s observe ...s in action. For one, dots can pass arguments along to other functions, referred to as forwarding. Here’s an example:
# Use `...` to forward arguments to other functions
fn01 <- function(x, y) {
c("val1" = x, "val2" = y)
}
fn02 <- function(z, ...) {
fn01(...)
}
fn02(x = 1, y = 2)val1 val2
1 2
Indeed,
This is pretty straight forward when you want to pass additional arguments to a specific function in your own function definitions, like the example code above.
However, if you need more control, then the arguments passed need to be captured using a list.
# Capturing for more control
fn03 <- function(...) {
dots <- list(...)
print(dots)
}
fn03(x = 1, y = 2)$x
[1] 1
$y
[1] 2
Once captured, the objects within the list can be later indexed, parsed, and computed on later. Josiah Parry’s explainer video goes into more detail about this using another example (04M45S).
In addition, this video here from @oggyinformatics has a really good overview and example on the use of a list to capture arguments (01M02s). It also contains a really good reminder and example for why you should use a list rather than a vector to capture arguments forwarded using ...s (03M05S): lists can contain any collection of the same data type, while vectors can only contain data of a single type. That is, vectors will transform all the datatypes to the same type. This is summarized in the following examples.
# lists allow for variables of different types to be captured and retain their original type
x <- c(1, 2, 3, 4)
fn04 <- function(vector, ...) {
args <- list(...)
for (i in vector) {
print(i)
}
print(args)
}
fn04(x)[1] 1
[1] 2
[1] 3
[1] 4
list()
# Now add a string and a boolean via dots
fn04(x, 'a', TRUE)[1] 1
[1] 2
[1] 3
[1] 4
[[1]]
[1] "a"
[[2]]
[1] TRUE
# why not use a vector? all items get converted to the same type
fn05 <- function(vector, ...) {
args <- c(...)
for (i in vector) {
print(i)
}
print(args)
}
fn05(x, 'a', TRUE)[1] 1
[1] 2
[1] 3
[1] 4
[1] "a" "TRUE"
Moveover, other strategies exist to capture and handle arguments passed using dots.
... handlingOnce I realized there’s more to the three-dots than the simple forwarding of arguments, I came across other handling strategies. The R Inferno book overviews three strategies for handling arguments passed via the three-dots. The first was discussed above: use a list to capture the argument values passed via dots.
Another strategy is to use match.call(). That is,
fn06 <- function(...) {
extras <- match.call(expand.dots = FALSE)$...
return(extras)
}
fn06(a = 1, b = 2, c = 3)$a
[1] 1
$b
[1] 2
$c
[1] 3
Or, in situations where your function processes the arguments, then you can use do.call()
While reviewing the do.call() strategy, it wasn’t immediately apparent to me when this would be useful. I also couldn’t get it to work using the example provided. Nonetheless, it’s an available strategy, so someone likely has a need for it and could likely get it to work.
rlang’s dynamic dotsThe rlang package provides dynamic dots. Some of the package’s functions extend the functionality of .... Besides argument forwarding and collection, rlang’s dynamic dots provides additional features. Check out the package’s docs for a deeper explanation, as the following is just a summary.
Dynamic dots implements what’s known as injection: the process of modifying a piece of code before R processes it. Dynamic dots has two injection operators, !!! and {. As such, this extended functionality allows for:
Although examples are available in the package’s documentation, I decided to notate their use as a reminder of how they work.
fn08 <- function(...) {
out <- rlang::list2(...)
return(out)
}
# list splicing
x <- list(a = "one", b = "two")
fn09 <- function(x) {
arguments <- fn06(!!!x)
return(arguments)
}
fn08(x)[[1]]
[[1]]$a
[1] "one"
[[1]]$b
[1] "two"
# name injections, glue syntax
nm <- "values"
fn08("{nm}" := x)$values
$values$a
[1] "one"
$values$b
[1] "two"
fn08("prefix_{nm}" := x)$prefix_values
$prefix_values$a
[1] "one"
$prefix_values$b
[1] "two"
# ignoring trailing commas
fn08(x = 6, )$x
[1] 6
Indeed, rlang provides some convenient extensions to ...’s functionality. Check it out.
... with ellipsisAnother package useful when implementing dots is rlib’s ellipsis package. The goal of ellipsis is to make the use of ... safer, as some unintended side effects can arise from their use. The package provides three convenience functions to do this:
check_dots_used()check_dots_unnamed()check_dots_empty()Each function performs some type of check on the arguments being passed with .... From the documentation, check_dots_used() throws an error if any ... are not evaluated. check_dots_unnamed() errors if any components of ... are named. check_dots_empty() errors if ... is used.
One concern of ... is it can “silently swallow” passed arguments. Say we want to ensure all the arguments passed to ... are evaluated. The check_dots_used() function can be helpful in this case. This function sets up a handler that evaluates when a function terminates, enforcing that all arguments have been evaluated. Otherwise, it will throw an error. For instance,
fn09 <- function(...) {
ellipsis::check_dots_used()
div_vals(...)
}
div_vals <- function(x, y, ...) {
x / y
}
# works, yay!
fn09(x = 10, y = 2)[1] 5
# doesn't work, because we're trying to process more arguments then are available
try(fn09(x = 10, y = 2, z = 1))Error in fn09(x = 10, y = 2, z = 1) : Arguments in `...` must be used.
✖ Problematic argument:
• z = 1
ℹ Did you misspell an argument name?
# also helpful when unevaluated unnamed arguments are passed
try(fn09(x = 10, y = 2, 1, 2, 3))Error in fn09(x = 10, y = 2, 1, 2, 3) : Arguments in `...` must be used.
✖ Problematic arguments:
• ..1 = 1
• ..2 = 2
• ..3 = 3
ℹ Did you misspell an argument name?
Named arguments passed with dots may be misspelled. As such, the check_dots_unnamed() function might be useful and a safer option for a function definition. For instance,
# not very safe
fn10 <- function(..., val_extra = 10) {
c(...)
}
# who hasn't misspelled an argument name before?
fn10(1, 2, 3, val = 4) val
1 2 3 4
fn10(1, 2, 3, val_extra = 4)[1] 1 2 3
# safer
fn11 <- function(..., val_extra = 10) {
rlang::check_dots_unnamed()
c(...)
}
fn11(1, 2, 3, val = 10)Error in `fn11()`:
! Arguments in `...` must be passed by position, not name.
✖ Problematic argument:
• val = 10
fn11(1, 2, 3, val_extra = 10)[1] 1 2 3
check_dots_empty() is useful for when you want users to fully name the details arguments. While reviewing, I felt this strategy not only enforces this but it also allows for more informative errors to be pushed to the console. It also seems to better handle situations where partial argument matching happens.
fn12 <- function(x, ..., foofy = 8) {
x + foofy
}
fn12(3, foofy = 8)[1] 11
fn12(3, foody = 8)[1] 11
fn13 <- function(x, ..., foofy = 8) {
rlang::check_dots_empty()
x + foofy
}
fn13(3, foofy = 8)[1] 11
fn13(3, foody = 8)Error in `fn13()`:
! `...` must be empty.
✖ Problematic argument:
• foody = 8
The ellipsis package provides some powerful, useful functionality for handling edge cases that come up when forwarding arguments via ...s. Check it out if you find yourself needing safer ... handling methods.
...
This R-bloggers’ post provides some additional overview of ...’s behavior. I attempt to summarize some of the points shared in the post below.
The function receiving the ...s does not itself need ...s as an argument.
fn14 <- function(x, ...) {
fn15(...)
}
fn15 <- function(y) {
print(y)
}
fn14(x = 1, y = 2)[1] 2
This is useful because if we pass anything other than y, we get an error.
fn14(x = 1, y = 2, z = 3)Error in fn15(...): unused argument (z = 3)
Using dots within both functions allows for the passing on an additional named argument without error. In this example, the z argument. This is not necessarily a utility of the dots, but rather a behavior to note. A behavior one would likely want to account for using functions from the ellipsis package, which was already discussed above.
fn14 <- function(x, ...) {
fn15(...)
}
fn15 <- function(y, ...) {
print(y)
}
fn14(x = 1, y = 2)[1] 2
fn14(x = 1, y = 2, z = 3)[1] 2
list(...) can be used to interpret the arguments passed using .... Why? This is helpful when you want to amend the arguments before forwarding them on. In other words, save the output of list(...) as a variable, amend this variable, then call the next function with the amended variable using do.call(). For instance, from the example from the original post:
fn16 <- function(x, ...) {
args <- list(...)
if ("y" %in% names(args)) {
args$y <- 2 * args$y
}
do.call(fn15, args)
}
fn15 <- function(y) {
print(y)
}
fn16(x = 1, y = 2)[1] 4
In this case, if an argument y is included, then it will be captured, and subsequently doubled before being outputted. A neat additional strategy for handling forwarded arguments, which has some utility for conditionally modifying argument values based on what’s forwarded.
So there you have it, a collection of notes on the use of ...s. Here’s a summary of what I’ve learned about using dots when programming in R:
...s are helpful for forwarding any number of additional arguments along to other functions....s. List capturing, in my view, seems to be the most useful and straight forward.rlang’s dynamic dots provides additional utilities for working with dots.ellipsis package provides convenience functions to make ...s safer....s have some interesting behavior to be aware of when implented within in an R function.What did I miss? What did I completely get wrong? I’d love the feedback.
If you found these notes and reflections useful, let’s connect:
... (dot-dot-dot) section from Advanced R@misc{berke2025,
author = {Berke, Collin K},
title = {Notes: {The} Use of `...` In {R}},
date = {2025-10-26},
langid = {en}
}