tryCatch and Input Validation
In Lesson 1, Priya's lending library taught its late_fee() function three voices: message(), warning() and stop(). She can now raise a clear signal when something is wrong. But raising a signal is only half the job.
Tonight she runs the nightly batch: 200 book returns, one after another, each priced by late_fee(). Buried in that list is a single corrupt record, a book scanned as -3 days late. With what she knows so far, that one bad row throws an error and the entire run stops dead, leaving the other 199 books unpriced. She needs two new skills: a way to catch that error and keep going, and a way to check each input up front so the failure is clean and obvious instead of a crash three steps later.
By the end of this lesson you will be able to:
- Use
tryCatch()to catch an error and return a fallback instead of crashing - Explain exactly what
tryCatch()returns, and when thefinallyblock runs - Wrap a batch so one bad record is skipped and the rest still process
- Guard a function's inputs with
stopifnot()so bad input fails fast with a clear message
Prerequisites: you can write a function with arguments and a return value, you know [if and else](Control-Flow-in-R.html), and you have met [stop(), warning() and message()](Errors-Warnings-and-Messages.html) from Lesson 1.
One bad record halts the whole run
Here is Priya's helper from Lesson 1. It refuses impossible input by calling stop(), which raises an error: a condition severe enough that R abandons the rest of the work. Defining the function and calling it on a sensible value is perfectly safe:
Now the nightly batch. The third book was scanned as -3 days late. Watch what that one record does to the loop:
returns <- c(5, 12, -3, 7) # the -3 book came back before it was due
for (d in returns) print(late_fee(d))
#> [1] 1.25
#> [1] 3
#> Error in late_fee(-3) : days_late must be 0 or more; you gave -3.
# the loop dies on -3, so the 7-day book is never priced
The error did its job, it stopped bad work, but it stopped all the work. Two books priced, the bad one rejected, and the last book silently lost. Priya does not want the run to die; she wants it to skip the one bad record and finish the other 199. For that she needs to catch the error.
tryCatch(): run this, and if it fails, do that
tryCatch() runs an expression for you and stands ready with a handler in case that expression raises a condition. If the expression finishes normally, you get its value. If it raises an error, R does not crash; instead it runs your error = handler and gives you that value back. You decide what "recovered" looks like.
A handler is just a function of one argument, the condition object. Inside it, conditionMessage() pulls out the human-readable message that stop() attached, so you can log exactly what went wrong:
The error from late_fee(-3) never reached the top level, so nothing crashed. The handler ran, printed a note, and handed back NA. That NA is now sitting in result, ready to be filtered out later.
tryCatch() can listen for more than errors. You name one handler per condition type, plus an optional finally:
| Argument | Fires when the expression raises... | Typical use |
|---|---|---|
error = function(e) |
an error (from stop()) |
recover, log, return a fallback |
warning = function(w) |
a warning (from warning()) |
treat a warning as fatal, or note it |
message = function(m) |
a message (from message()) |
capture or silence a status note |
finally = |
always (error or not) | cleanup that must happen either way |
error = handler ignores warnings and messages; it only catches errors. If you want to catch a warning, add a warning = handler too.What runs, and in what order
The single thing learners get wrong about tryCatch() is the order of events, so let us pin it down. R evaluates the first expression. The moment a matching condition is raised, it abandons the rest of that expression, jumps to the handler, and the handler's return value becomes the value of the whole tryCatch() call. Whether or not anything failed, the finally block runs last.
finally is for cleanup you cannot afford to skip: closing a file, logging that a record was processed, releasing a connection. It runs on the happy path and the error path alike. Here is the same call on a good record and a bad one:
Notice the finally note prints in both calls, the successful one and the failed one. That is the guarantee: finally is the code that always happens.
When nothing goes wrong
You write tryCatch(late_fee(5), error = function(e) NA_real_). The call late_fee(5) succeeds, no error at all. What value does the tryCatch() return?
tryCatch() simply hands back the expression's own value, here 5 * 0.25.tryCatch() is not empty-handed on success. It returns the value of the expression it ran; the handler is simply bypassed.Catch per record, keep the batch alive
Now the move Priya actually needs. Instead of wrapping the whole loop in one tryCatch() (which would still stop at the first bad row), she wraps each record individually. One corrupt row becomes a single NA, and every other row sails through:
Five records in, five results out, no crash. The two impossible days became NA, the three valid books were priced, and na.rm = TRUE quietly drops the gaps when Priya totals the night's fees. This pattern, wrap the risky step per item and return a sentinel on failure, is how you make a long batch resilient instead of brittle.
tryCatch() decides what survives a failure. Around the whole loop, one bad row sinks the batch. Around each item, one bad row sinks only itself. Catch as close to the risky operation as you can.Guard the inputs up front with stopifnot()
Catching errors handles the records you cannot fix. But many failures are avoidable, and the kindest thing a function can do is refuse bad input immediately, before it computes anything, with a message that says exactly which rule broke. In Lesson 1 Priya wrote that guard by hand with if and stop(). stopifnot() collapses a whole stack of those guards into one line.
You give stopifnot() a series of conditions that must all be TRUE. If every one holds, it returns nothing and your function carries on. The instant one is FALSE, it raises an error and stops. Name each condition and that name becomes the error message:
All three checks held, so stopifnot() was silent and you got the fee. Now feed it the impossible record and the matching name comes back as a clean, specific error:
late_fee(-3)
#> Error in late_fee(-3) : days_late must be 0 or more
stopifnot(days_late >= 0), still works but reports the raw expression: Error: days_late >= 0 is not TRUE. The named form lets you write a sentence a human can act on. Put these guards at the very top of the function, before any real work, so bad input fails fast and the rest of the body can trust its arguments.Prevent, or recover?
A function is handed a value that is plainly wrong, a piece of text where it needs a number. You want it to refuse immediately, with a clear message, rather than compute nonsense or limp along. Which tool fits?
stopifnot() checks the inputs before any real work and raises a clear, named error the moment a rule is broken, so the function never computes on input it already knows is wrong.Make late_fee robust
Put both halves together. Below, late_fee() should validate its input up front, and the nightly job should recover from any record that still slips through. The validation line is missing.
Replace the ____ with the function that checks a list of conditions and stops on the first failure (use the named-condition form already written for you). Then check your answer.
Show answer
late_fee <- function(days_late, rate = 0.25) {
stopifnot("days_late must be 0 or more" = days_late >= 0)
days_late * rate
}
safe_total <- sum(sapply(c(5, -3, 7), function(d) {
tryCatch(late_fee(d), error = function(e) NA_real_)
}), na.rm = TRUE)
safe_total
#> [1] 3References
A few authoritative, free places to take this further:
- Advanced R (2e): Conditions - Hadley Wickham on R's condition system, including how
tryCatch()matches and handles errors, warnings and messages. - Base R help: conditions and tryCatch - the canonical reference page for
tryCatch,withCallingHandlers,conditionMessageand friends. - Base R help: stopifnot - the official page for the validation function you used, including the named-message form.
- Tidyverse style guide: Error messages - a practical checklist for writing the kind of clear, actionable messages your
stopifnot()names should contain.
Lesson 2 complete
You can now make a function survive the real world. tryCatch() lets you catch an error and return a fallback instead of crashing, you know its handler runs only on a matching condition and that finally always runs, and wrapping each record in its own tryCatch() keeps a long batch alive past any single bad row. On the other side, stopifnot() guards a function's inputs up front with named, human-readable messages, so avoidable failures fail fast and clearly. Validate what you can; recover from what you cannot.
Next, Lesson 3: Debugging Tools in R. When something still goes wrong and you do not yet know why, you will trace an error back to its source with traceback(), freeze a running function and look around with browser(), and drive the IDE's debugger, turning a mysterious failure into a fixable one.