Skip to contents

The idea

assert gives you small, explicit helpers for checking the inputs and outputs of your functions. You place assertions at the top of a function; if one fails, it throws an error and the function stops before any real work happens.

The examples below import the functions they use with box::use(assert[...]), so it is always clear where each function comes from. If you prefer, library(assert) works too.

Every assertion shares the same contract:

  • it checks one condition,
  • it returns its input invisibly on success, so checks stack neatly,
  • it throws a clear error pointing at your function on failure.

Guarding inputs

box::use(
  assert[
    assert_numeric,
    assert_no_missing_values,
    assert_scalar_numeric
  ]
)

scale_values <- function(values, factor) {
  assert_numeric(values)
  assert_no_missing_values(values)
  assert_scalar_numeric(factor)

  return(values * factor)
}

scale_values(c(1, 2, 3), 10)
#> [1] 10 20 30

Read top to bottom, the assertions describe the preconditions: “values is numeric, has no missing values; factor is a single number.” When something is wrong, the message names the argument and the calling function:

scale_values(c(1, NA, 3), 10)
#> Error in `scale_values()`:
#> ! `values` must not contain missing values.
scale_values(c(1, 2, 3), c(10, 20))
#> Error in `scale_values()`:
#> ! `factor` must be a single numeric value.

Scalars reject NA

A assert_scalar_* check requires a single, non-NA value, because a lone value that is secretly NA is almost always a mistake. The plain vector checks (assert_numeric(), assert_character(), …) still allow NA, since a missing value among many is often legitimate.

box::use(assert[assert_scalar_numeric])

assert_scalar_numeric(NA)
#> Error:
#> ! `NA` must be a single numeric value.

Optional arguments with null_ok

Arguments that default to NULL are optional. By default an assertion rejects NULL; pass null_ok = TRUE to allow it while still validating any value that is supplied.

box::use(assert[assert_scalar_character])

greet <- function(name, title = NULL) {
  assert_scalar_character(name)
  assert_scalar_character(title, null_ok = TRUE)

  return(if (is.null(title)) name else paste(title, name))
}

greet("Ada")
#> [1] "Ada"
greet("Lovelace", title = "Ms")
#> [1] "Ms Lovelace"

Constraining values

Beyond types, you can assert the content of a vector.

box::use(
  assert[
    assert_all_positive,
    assert_between,
    assert_values_in_set
  ]
)

assert_all_positive(c(1, 2, 3))
assert_between(c(0.2, 0.5, 0.9), lower = 0, upper = 1) # closed: [0, 1]
assert_between(c(0.2, 0.5, 0.9), lower = 0, upper = 1, lower_inclusive = FALSE) # ]0, 1]
assert_values_in_set(c("buy", "sell"), c("buy", "sell", "hold"))
invisible(NULL)

assert_between() treats each bound as inclusive by default; set lower_inclusive or upper_inclusive to FALSE for an open end, leave a bound NULL for a one-sided range, and pass na_ok = TRUE to permit NA elements. It compares with < / > only, so the same call works for dates and date-times.

Relationships between arguments

When several optional arguments interact, the argument-group helpers express the rule directly. They treat NULL as “not supplied”.

box::use(assert[assert_exactly_one])

open_file <- function(path = NULL, connection = NULL) {
  assert_exactly_one(path, connection)
  return("ok")
}

open_file(path = "data.csv")
#> [1] "ok"
open_file()  # neither supplied
#> Error in `open_file()`:
#> ! Exactly one of these must be supplied: path, connection (0 were
#>   supplied).

Stacking checks with the pipe

Because every assertion returns its input invisibly, checks compose naturally with the native pipe |>. Each one validates the value and passes it to the next, so a chain reads top to bottom as a list of guarantees about the same object. This works for any object, and is especially handy for data frames, where you often want to confirm structure and column types in one place.

box::use(
  assert[
    assert_data_frame,
    assert_has_columns,
    assert_column_types,
    assert_unique_rows
  ]
)

trades <- data.frame(
  symbol = c("AAA", "BBB"),
  quantity = c(10L, 5L),
  price = c(1.5, 2.0)
)

trades |>
  assert_data_frame() |>
  assert_has_columns(c("symbol", "quantity", "price")) |>
  assert_column_types("character", "symbol") |>
  assert_column_types("integer", "quantity") |>
  assert_column_types("numeric", "price") |>
  assert_unique_rows() |>
  invisible()

One of several types

Stacking assertions means “all of these must hold”. When a value may legitimately be one of several types or shapes, assert_any_of() is the “or”: it accepts the value if any of the listed checks passes, and on failure reports every alternative it tried. Each alternative is a function taking the value — an existing assertion by name, or a small closure stacking several checks.

box::use(assert[assert_any_of, assert_numeric, assert_character])

identifier <- function(x) {
  assert_any_of(x, assert_numeric, assert_character)
  return(x)
}

identifier(42)
#> [1] 42
identifier("abc")
#> [1] "abc"
identifier(TRUE) # neither numeric nor character
#> Error in `identifier()`:
#> ! `x` must satisfy at least one of: `x` must be a numeric vector. | `x`
#>   must be a character vector.

The escape hatch

For any condition without a dedicated assertion, use assert_true() with a custom message.

box::use(assert[assert_scalar_numeric, assert_true])

set_threshold <- function(x) {
  assert_scalar_numeric(x)
  assert_true(x %% 2 == 0, message = "`x` must be an even number.")
  return(x)
}

set_threshold(4)
#> [1] 4
set_threshold(3)
#> Error in `set_threshold()`:
#> ! `x` must be an even number.