R Academy · Lesson

Writing Robust Functions with stop() and warning()

Signal custom conditions from within your own functions.

Lesson 4 of 413 steps

Writing Robust Functions with stop() and warning() is a free R Academy lesson on CoddyKit. This is lesson 4 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, and your progress syncs across the web and the CoddyKit app. The R Academy course includes 4 lessons in total.

Why Input Validation Matters

Defensive functions check their inputs and fail loudly with informative errors rather than silently producing wrong results. A function that catches bad input at the boundary is far easier to debug than one that propagates garbage through a pipeline.

# Without validation: wrong result, no error
bad_mean <- function(x) sum(x) / length(x)
bad_mean('hello')   # no error, just NA

# With validation: clear error
good_mean <- function(x) {
  if (!is.numeric(x)) stop('x must be numeric')
  sum(x) / length(x)
}
tryCatch(good_mean('hello'), error = function(e) cat(e$message, '\n'))

Informative Error Messages

Generic errors like stop('bad input') are frustrating to debug. Use paste0() or sprintf() inside stop() to include the actual value and expected type in the message. This saves time when tracing errors in pipelines.

check_numeric <- function(x, arg_name = 'x') {
  if (!is.numeric(x)) {
    stop(sprintf(
      "'%s' must be numeric, but got class '%s'",
      arg_name, class(x)
    ))
  }
}
tryCatch(check_numeric('abc', 'score'),
  error = function(e) cat(e$message, '\n'))

Validating Length and Range

Common validations: check that a vector is not empty (length(x) == 0), that a scalar is within range, or that a data frame has the expected columns. Each check should have its own tailored error message.

validate_prob <- function(p) {
  if (!is.numeric(p)) stop(paste0('p must be numeric, got: ', class(p)))
  if (length(p) != 1) stop(paste0('p must be length 1, got: ', length(p)))
  if (p < 0 || p > 1) stop(paste0('p must be in [0,1], got: ', p))
  p
}
cat(validate_prob(0.7), '\n')
tryCatch(validate_prob(1.5), error = function(e) cat(e$message, '\n'))

Using warning() for Non-Fatal Issues

Use warning() when the function can still return a sensible result but the caller should know something unusual happened — like coercing a value, imputing a default, or detecting a borderline input.

clamp <- function(x, lo, hi) {
  if (any(x < lo)) warning(paste0(sum(x < lo), ' value(s) below lo, clamped'))
  if (any(x > hi)) warning(paste0(sum(x > hi), ' value(s) above hi, clamped'))
  pmax(lo, pmin(hi, x))
}
result <- clamp(c(2, -5, 7, 100, 4), 0, 10)
cat('Result:', result, '\n')

call. = FALSE in stop()

By default, stop() prepends Error in funcname(...): to the message. Pass call. = FALSE to suppress this prefix when the calling context is obvious or when the function is a user-facing utility.

# With call. = TRUE (default): shows calling function
f1 <- function(x) stop('bad input')
tryCatch(f1(1), error = function(e) cat(conditionMessage(e), '\n'))

# With call. = FALSE: cleaner message
f2 <- function(x) stop('bad input', call. = FALSE)
tryCatch(f2(1), error = function(e) cat(conditionMessage(e), '\n'))

Custom Condition Classes for stop()

Create a custom error class using structure() so callers can catch just your function's errors without catching all errors. This is the professional way to write package-level errors.

value_error <- function(msg, call = sys.call(-1)) {
  structure(
    class = c('value_error', 'error', 'condition'),
    list(message = msg, call = call)
  )
}
check_positive <- function(x) {
  if (x <= 0) stop(value_error(paste0('Expected positive, got: ', x)))
  x
}
tryCatch(check_positive(-3),
  value_error = function(e) cat('ValueError:', e$message, '\n'),
  error        = function(e) cat('Other error:', e$message, '\n'))

Checking Data Frame Columns

When a function expects a data frame, validate that required columns exist before using them. Use %in% to check column names and setdiff() to report which columns are missing.

require_cols <- function(df, cols) {
  missing <- setdiff(cols, names(df))
  if (length(missing) > 0) {
    stop(paste0('Missing columns: ', paste(missing, collapse = ', ')))
  }
  invisible(df)
}
df <- data.frame(x = 1:3, y = 4:6)
tryCatch(
  require_cols(df, c('x', 'z', 'w')),
  error = function(e) cat(e$message, '\n')
)

stopifnot() for Concise Assertions

stopifnot(condition1, condition2, ...) throws an error if any condition is FALSE. It is a concise way to assert preconditions at the top of a function without writing separate if (!...) stop(...) blocks.

compute_area <- function(width, height) {
  stopifnot(
    is.numeric(width),
    is.numeric(height),
    width > 0,
    height > 0
  )
  width * height
}
cat(compute_area(5, 3), '\n')
tryCatch(compute_area(-1, 3), error = function(e) cat(e$message, '\n'))

Named stopifnot() for Better Messages

In R 4.0+, named expressions in stopifnot() replace the auto-generated message with your custom description. Use the expression text as the name to produce informative failures.

validate_age <- function(age) {
  stopifnot(
    'age must be numeric'    = is.numeric(age),
    'age must be positive'   = age > 0,
    'age must be under 150'  = age < 150
  )
  invisible(age)
}
tryCatch(validate_age(-5), error = function(e) cat(e$message, '\n'))
tryCatch(validate_age('x'), error = function(e) cat(e$message, '\n'))

Combining Validation and Business Logic

A well-structured function separates validation from logic. Validate at the top, then perform the operation. This makes the function easier to read and ensures errors are detected before any computation begins.

discount_price <- function(price, pct) {
  if (!is.numeric(price) || price <= 0)
    stop(paste0('price must be positive numeric, got: ', price))
  if (!is.numeric(pct) || pct < 0 || pct > 100)
    stop(paste0('pct must be in [0,100], got: ', pct))
  if (pct > 50) warning('discount > 50% is unusual')
  price * (1 - pct / 100)
}
cat(discount_price(100, 20), '\n')
cat(suppressWarnings(discount_price(100, 60)), '\n')

tryCatch Inside Utility Functions

Sometimes a utility function should handle errors internally and return a fallback rather than propagating them. Wrap the core logic in tryCatch() and return an agreed-upon sentinel like NA or NULL on failure.

safe_log <- function(x) {
  tryCatch({
    if (!is.numeric(x)) stop('not numeric')
    if (x <= 0) stop('must be positive')
    log(x)
  }, error = function(e) {
    warning(paste0('safe_log failed for x=', x, ': ', e$message))
    NA_real_
  })
}
results <- sapply(list(10, -1, 'a', 100), safe_log)
cat(results, '\n')

Quick Check

What does call. = FALSE do when passed to stop()?

Robust Functions: Key Takeaways

Key takeaways for writing robust functions:

  • Validate inputs at the top; fail loudly with informative stop() messages
  • Include the actual value in error messages: paste0('Expected numeric, got: ', class(x))
  • Use warning() for non-fatal issues where computation can continue
  • call. = FALSE suppresses the calling function prefix in error messages
  • stopifnot() for concise assertions; named form for custom messages (R 4.0+)
  • Custom condition classes enable selective catching by callers
robust_divide <- function(x, y) {
  stopifnot('x must be numeric' = is.numeric(x),
            'y must be numeric' = is.numeric(y))
  if (y == 0) stop('Division by zero', call. = FALSE)
  if (abs(y) < 1e-10) warning('y is very small; result may be inaccurate')
  x / y
}
cat(robust_divide(10, 2), '\n')
tryCatch(robust_divide(10, 0), error = function(e) cat(e$message, '\n'))
Free to start

Learn R with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
43
Lessons
159

Frequently Asked Questions

Is the “Writing Robust Functions with stop() and warning()” lesson free?

Yes — any 3 lessons of the R Academy learning path, including “Writing Robust Functions with stop() and warning()”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The R Academy course includes 4 lessons in total.

What will I learn in “Writing Robust Functions with stop() and warning()”?

Signal custom conditions from within your own functions. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start R Academy?

No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.

How long does the “Writing Robust Functions with stop() and warning()” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this R Academy lesson?

Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Errors, Warnings, and Messages in R
  2. tryCatch() for Error Recovery
  3. withCallingHandlers() and Restarts
  4. Writing Robust Functions with stop() and warning()
← Back to R Academy