← → or space · progress saves for Continue on the roadmap
Goal
Represent success and failure as values instead of throwing for expected cases.
Step 1 - Record-style result
({bool ok, String? error}) validateNonEmpty(String? value) {
if (value == null || value.trim().isEmpty) {
return (ok: false, error: 'required');
}
return (ok: true, error: null);
}
void main() {
final r = validateNonEmpty(' ');
if (r.ok) {
print('fine');
} else {
print(r.error);
}
}Step 2 - Generic Result<T>
class Result<T> {
final T? value;
final String? error;
const Result._({this.value, this.error});
const Result.ok(T this.value) : error = null;
const Result.err(String this.error) : value = null;
bool get isOk => error == null;
}
void main() {
Result<int> r = const Result.ok(7);
if (r.isOk) print(r.value);
}Step 3 - When to throw vs return Result
- Throw for truly exceptional or programmer errors, or when you want stack traces at the boundary.
- Return
Result(orFuture<Result>) when failure is a normal branch callers must handle.
Practice tasks
- Write
Result<int> parseIntSafe(String raw)without throwing on bad input. - Map
Result<String>toResult<int>with a methodResult<U> map<U>(U Function(T) f)that preserves errors (implement for non-null success only first).