Course outline

R Programming / Evaluation, ownership, and reproducible results / Lesson 15

Conditions, cleanup, and contract tests

Preserve meaningful failures and release owned state under ordinary function exit paths.

About 4 min readContent reviewed
On this page

Lesson goals

  • Distinguish messages, warnings, and errors.
  • Catch a known condition without hiding unrelated failures.
  • Register cleanup and test success and failure boundaries.

A condition carries information about an event

R’s condition system represents messages, warnings, and errors as objects with classes, messages, and calls. An error interrupts ordinary evaluation unless handled; a warning reports an issue that may allow computation to continue. Their roles differ, so suppressing all warnings is not a general repair. Begin by identifying whether an event is an expected invalid input, a recoverable situation with a defined alternative, or a programming defect. A useful condition preserves enough context for the caller to choose a response without treating a failed calculation as an ordinary successful value.

Catch only the failure the caller understands

The first fixture defines a zero_denominator condition and catches that class in a wrapper. A valid division returns an explicit success record; the expected invalid denominator returns an explicit failure record with a reason. An unrelated error remains visible. This is safer semantically than catching every error and returning zero or NA without status. Real validation also needs a scalar numeric argument contract; the fixture isolates condition dispatch rather than promising a general division API. Structured status should have stable names and documented value availability so downstream code cannot accidentally average failure records as measurements.

r Illustrative base-R script; R runtime not available during review
divide <- function(a, b) {
  if (b == 0) stop(errorCondition("zero denominator", class = "zero_denominator"))
  a/b
}
safe_divide <- function(a, b) {
  tryCatch(list(ok = TRUE, value = divide(a, b)),
           zero_denominator = function(e) list(ok = FALSE, reason = conditionMessage(e)))
}
good <- safe_divide(6, 2)
bad <- safe_divide(6, 0)
cat("good=", good$ok, "; value=", good$value, "\n", sep = "")
cat("bad=", bad$ok, "; reason=", bad$reason, "\n", sep = "")
Expected output
good=TRUE; value=3
bad=FALSE; reason=zero denominator

A narrow condition class supports an explicit expected failure without converting unrelated programming errors into plausible numeric values.

Cleanup belongs near resource acquisition

A function that changes options, opens a connection, or creates a graphics device should define what state it owns and how that state is restored. on.exit registers an expression to run when the function exits under ordinary completion or error unwinding. The second fixture uses a synthetic in-memory flag to demonstrate that idea without touching an external resource. Register cleanup immediately after acquiring the owned resource, and use add=TRUE when adding rather than replacing existing cleanup. Cleanup ordering matters when resources depend on one another. Process termination or a crash is outside an ordinary function-unwinding guarantee.

r Illustrative base-R script; R runtime not available during review
resource <- new.env(parent = emptyenv())
resource$closed <- FALSE
work <- function() {
  on.exit({ resource$closed <- TRUE }, add = TRUE)
  stop("synthetic failure")
}
message <- tryCatch(work(), error = function(e) conditionMessage(e))
cat("message=", message, "\n", sep = "")
cat("cleanup=", resource$closed, "\n", sep = "")
Expected output
message=synthetic failure
cleanup=TRUE

The synthetic resource is an in-memory flag; on.exit cleanup is expected on ordinary error unwinding without opening files, devices, or services.

Handlers and cleanup have different responsibilities

tryCatch chooses a replacement result when a matching condition transfers control to its handler. Calling handlers can inspect conditions in a different control-flow style, and restarts provide further recovery mechanisms. These tools should follow a defined API rather than obscure a simple input check. Cleanup should restore owned state independently of whether the caller accepts or propagates an error. A cleanup failure can complicate diagnosis, so preserve the original context where practical and keep cleanup operations narrow. Avoid silently altering global warning options to make a troublesome script appear clean.

Test contracts rather than only happy-path arithmetic

A small base-R test can use stopifnot and explicit comparisons to assert values, types, dimensions, names, and intended conditions. Approximate numeric results require a stated tolerance. Include a valid case, an invalid type or length, empty input, missingness, and the important resource-failure path. Test that unrelated errors are not swallowed by a narrow handler. A passed test establishes the stated behavior for its fixture, not exhaustive correctness. No R engine was available during review, so the supplied scripts and condition text are documented expectations; no error handler, cleanup path, or test assertion was executed in R.

Common errors and fixes

Every failed summary becomes zero

Why it happens
A broad handler replaced all errors with a plausible measurement.
How to fix it
Catch expected classes and return explicit status or propagate failure.
Prevent it next time
Test that unrelated programming errors remain visible.

Adding cleanup disables an earlier cleanup step

Why it happens
A later on.exit registration replaced the previous expression.
How to fix it
Use an intentional add and ordering policy.
Prevent it next time
Test multiple owned resources and ordinary failure unwinding.

A warning disappears but bad values remain

Why it happens
Suppression hid the symptom without fixing its cause.
How to fix it
Inspect the condition and the resulting data before choosing a policy.
Prevent it next time
Keep targeted warning cases in the contract suite.

Check your understanding

1. Why should a zero-denominator handler not catch an unrelated undefined variable error?

Show worked solution

The wrapper has a defined response for the denominator condition only. An undefined variable indicates a different problem that should remain diagnosable. Catching it as expected invalid data would make an implementation defect look like an ordinary user case.

2. What should a cleanup test assert after a synthetic failure?

Show worked solution

It should assert that the original failure remains correctly identified and that the state owned by the function has been restored. In the flag fixture, the captured message is synthetic failure and closed is TRUE. A real connection or device test would need its own supported runtime and ownership checks.

Key takeaways

  • Conditions are structured API events.
  • Narrow handling preserves unexpected defects.
  • Cleanup and testing must include failure paths.

Official references