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.
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.
void main() {
try {
final value = int.parse('bad');
print(value);
} on FormatException {
print('invalid integer');
} finally {
print('parse finished');
}
}
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.
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');
}
}
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.