Course outline

Dart / Foundations: values, absence, collections, and valid objects / Lesson 6

Separate expected rejection from exceptions and cleanup

Use tryParse, typed on/catch boundaries, rethrow, and finally without turning every defect into a plausible success.

About 5 min readContent reviewed
On this page

Lesson goals

  • Choose an ordinary invalid-input outcome or a thrown failure according to the caller contract.
  • Preserve diagnostic causes and owned cleanup while avoiding overly broad catches.

Expected input failure and programming misuse differ

Malformed user text is often a normal validation outcome rather than an exceptional application failure. int.tryParse supports that branch directly, while int.parse throws FormatException for invalid numeric text. An internal function receiving a forbidden argument may instead throw ArgumentError to expose caller misuse. Dart does not require checked-exception declarations, so the function signature alone does not list every possible failure. Document important failure behavior and distinguish it from Error conditions that often indicate a programming defect. A broad handler should not pretend the application can recover correctly from every object that can be thrown.

Catch only the failure that the boundary understands

on selects an exception type, and catch can capture the exception and optional stack trace. Keep the protected block narrow enough that a caught FormatException really means the intended parse failed. If parsing, storage, and rendering share one catch, a different formatting defect may be mislabeled as invalid user input. The example uses fixed text and a safe message instead of exposing a raw exception to an end user. Finally prints a marker to show order; it is not evidence of actual file, socket, or subscription cleanup. No Dart program was executed during authoring.

dart A typed parse handler and cleanup-order marker
void main() {
  try {
    final value = int.parse('bad');
    print(value);
  } on FormatException {
    print('invalid integer');
  } finally {
    print('parse finished');
  }
}
Expected output
invalid integer
parse finished

Expected output is invalid integer then parse finished. The parse does not produce a value, so the print inside try is not reached. This is unexecuted illustrative source.

Translate deliberately and preserve useful causes

A boundary may translate a provider-specific failure into an application outcome, but it should retain safe diagnostic context and avoid erasing the cause. rethrow propagates the currently handled exception while preserving its stack information more appropriately than pretending a new unrelated error occurred. Do not log secrets, personal payloads, or unbounded object dumps merely because a stack trace is useful. A fallback is a product policy, not an automatic consequence of catching something. Returning an empty successful batch after a storage failure can be more damaging than a truthful explicit failure.

Use runtime validation when correctness depends on it

assert is intended for conditions checked in assertion-enabled execution and must not be the only production validation for untrusted data. An explicit guard remains part of normal control flow. The local subtotal below uses a simple positive-count and nonnegative-unit rule, with small values that stay in an uncomplicated numeric range. It is a teaching calculation, not a financial system. Error translation occurs at main, while the reusable function returns a value on valid input. Tests should examine both accepted output and the rejection path rather than merely counting how many exceptions were caught.

dart Explicit argument checks instead of an assertion-only rule
int subtotal(int count, int unit) {
  if (count <= 0 || unit < 0) {
    throw ArgumentError('invalid count or unit');
  }
  return count * unit;
}
void main() {
  print(subtotal(2, 15));
  try {
    subtotal(0, 15);
  } on ArgumentError {
    print('rejected arguments');
  }
}
Expected output
30
rejected arguments

Expected output is 30 then rejected arguments. No real payment or external operation occurs. The code was not analyzed or run with Dart tooling.

Cleanup must not manufacture a different outcome

finally runs when control leaves the associated try/catch under normal exception semantics. Avoid returning from finally or throwing an unrelated cleanup error that hides the original important failure. Resource APIs can have synchronous or asynchronous closure contracts, and a stream subscription may require cancellation; match the actual resource instead of assuming one generic close pattern. Async failures need the appropriate Future or Stream handling boundary, developed later, and are not universally caught by a synchronous try around task creation. Record exactly which cleanup was verified rather than inferring success from a printed marker.

Common errors and fixes

Every failure is displayed as malformed input.

Why it happens
A catch block spans unrelated operations or ignores exception type.
How to fix it
Narrow the protected work and translate only failures with understood meaning.
Prevent it next time
Keep parsing, business policy, storage, and presentation boundaries distinct.

Production validation disappears when assertions are disabled.

Why it happens
assert was the only guard for required input restrictions.
How to fix it
Use explicit runtime checks or a validated typed input boundary.
Prevent it next time
Test the behavior expected in the actual production assertion configuration.

A cleanup return masks a previous exception.

Why it happens
The cleanup path replaced the original control flow.
How to fix it
Remove the return and preserve the meaningful result or failure.
Prevent it next time
Review cleanup under both successful and failed operations.

Check your understanding

1. Why should an API not catch every thrown object and return an empty list?

Show worked solution

The empty list may falsely mean a successful query with no records, hiding storage outages, programmer defects, or authorization failures. Define which failures can become an ordinary result and retain explicit failure for the rest. Keep safe diagnostics and verify caller behavior. Catching an object proves only that control reached the handler, not that recovery preserved correctness.

2. A finally marker prints after a failed operation. What still needs verification?

Show worked solution

Inspect the actual owned resource state, whether closure completed or failed, whether partial business effects remain, and whether the caller received the correct outcome. For asynchronous resources, await the required cleanup contract. A local print statement cannot establish remote rollback, file integrity, or subscription cancellation. Report the exact checks performed rather than treating the marker as a general recovery certificate.

Key takeaways

  • Failure categories and typed catch scope determine whether translation is truthful.
  • Validation and cleanup must remain effective under the real execution configuration.

Official references