Skip to content
Back to skills

Flutter Result Errors

ASecurity

Use when writing or reviewing Flutter data-layer, repository or provider code that can fail, when adding Result/AsyncResult types or failure types, when mapping exceptions to user-facing errors, or when an exception is crossing a layer boundary. Sets up a sealed AppFailure taxonomy, Result typedefs over result_dart, one catch boundary (RemoteDataSourceGuard) and the way presentation folds failures.

  • 6 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 3, 2026
developmentgotestingapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 7 files and shows the line behind each finding

Scanned October 3, 2026

npx -y skills add Ethiel97/flutter-clean-riverpod-skills --skill flutter-result-errors --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flutter Result Errors?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Flutter Result Errors
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ethiel97-flutter-result-errors/badge)](https://www.skillsdirectory.com/skills/ethiel97-flutter-result-errors)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: flutter-result-errors
description: Use when writing or reviewing Flutter data-layer, repository or provider code that can fail, when adding Result/AsyncResult types or failure types, when mapping exceptions to user-facing errors, or when an exception is crossing a layer boundary. Sets up a sealed AppFailure taxonomy, Result typedefs over result_dart, one catch boundary (RemoteDataSourceGuard) and the way presentation folds failures.
---

# Result-based error handling for Flutter

Nothing throws across a layer boundary. Every data-source and repository
method returns a `Result` that is either a value or an `AppFailure`.

## Why

- A thrown exception is invisible in a method signature; a `Result` is not.
  Callers cannot forget the failure path.
- `AppFailure` is **sealed**, so a `switch` over it is exhaustive. Adding a
  failure type becomes a compile error everywhere it is not handled.
- Exactly one place catches infrastructure exceptions, so mapping,
  logging and error reporting live in one funnel instead of N try/catch.

## Setup (once per project)

Copy these from this skill's `assets/` and replace `your_app` with the
package name in `pubspec.yaml`. Add `result_dart` to dependencies.

| Asset | Destination |
|---|---|
| `app_failure.dart`, `result.dart`, `errors.dart` | `lib/core/errors/` |
| `api_exception.dart`, `remote_data_source_guard.dart` | `lib/core/network/` |
| `submission_notifier.dart` | `lib/core/utils/` |

Feature code imports `core/errors/errors.dart`, **never**
`package:result_dart` directly, so the package stays swappable.
Where a file also imports `flutter_riverpod`, use
`import 'package:flutter_riverpod/flutter_riverpod.dart' hide AsyncResult;`
to avoid a name clash with the app typedef.

## Layer responsibilities

| Layer | Does | Never |
|---|---|---|
| Remote data source | Wraps every request in `guard(...)`; parses JSON into models | Contains `try/catch`; returns entities |
| Repository | Calls data sources, maps models → entities with `result.map(...)` | Catches, throws, or leaks models/DTOs |
| Domain | Pure Dart | Throws or catches |
| Presentation | Folds the `Result` into UI state | Reads `failure.message` for UI copy |

## Writing a data source

```dart
class ProfileRemoteDataSourceImpl
    with RemoteDataSourceGuard
    implements ProfileRemoteDataSource {
  ProfileRemoteDataSourceImpl(this._apiClient);
  final ApiClient _apiClient;

  @override
  AsyncResult<ProfileModel> fetchProfile() {
    return guard(() async {
      final response = await _apiClient.get(ProfileEndpoints.me);
      final data = response['data'] as Map<String, dynamic>;
      return ProfileModel.fromJson(data);
    });
  }
}
```

`guard` already handles timeouts (→ `NetworkFailure`), `ApiException`
(→ via `toAppFailure()`), and anything else (→ `UnknownFailure` carrying the
error and stack trace). Do not add a second `try/catch` around it.

Operations that return no data use `AsyncResult<Unit>`:

```dart
AsyncResult<Unit> deleteAccount() => guard(() async {
  await _apiClient.delete(AccountEndpoints.me);
  return unit;
});
```

## Writing a repository

The repository only converts; it passes failures through untouched.

```dart
@override
AsyncResult<Profile> getProfile() async {
  final result = await _remote.fetchProfile();
  return result.map((model) => model.toEntity());
}

@override
AsyncResult<Unit> deleteAccount() async {
  final result = await _remote.deleteAccount();
  return result.map((_) => unit);
}
```

## Consuming in presentation

**Reads** — bridge into Riverpod's `AsyncValue`. `getOrThrow()` inside a
provider is the one sanctioned throw: it surfaces as `AsyncError` to the UI.

```dart
class StatsNotifier extends AsyncNotifier<Stats> {
  @override
  Future<Stats> build() async {
    final result = await ref.watch(statsRepositoryProvider).getStats();
    return result.getOrThrow();
  }
}
```

**Actions** (submit, toggle, delete) — extend `SubmissionNotifier<T>` so a
button press gets loading / data / error state without `setState`:

```dart
class FavoriteSubmissionNotifier extends SubmissionNotifier<Unit> {
  Future<void> remove(String id) =>
      run(() => ref.read(favoritesProvider.notifier).remove(id));
}

final favoriteSubmissionProvider =
    NotifierProvider.autoDispose<FavoriteSubmissionNotifier, AsyncValue<Unit?>>(
      FavoriteSubmissionNotifier.new,
    );
```

Idle is `AsyncData(null)`. The notifier is `autoDispose` so a stale error
resets when the screen is left. `run` ignores a second call while loading
(double-tap guard) and survives the provider being disposed mid-request.

**UI copy** comes from l10n keyed on the failure *type*:

```dart
String failureMessage(AppLocalizations l10n, AppFailure failure) =>
    switch (failure) {
      NetworkFailure() => l10n.errorNetwork,
      AuthFailure() => l10n.errorSessionExpired,
      CancelledFailure() => '', // an answer, not a fault: stay silent
      _ => l10n.errorGeneric,
    };
```

Drop the `_` arm if you want the compiler to force every new failure to
be handled. Keep it when a feature-specific mapper falls back to a shared
baseline (match domain failures first, then delegate).

## Extending the taxonomy

Add a `final class XFailure extends AppFailure` (with its own `kind`) when
the UI must treat a case differently from the generic ones, e.g.
`CancelledFailure` (silent), a store refusing purchases, a social sign-in
sheet dismissed. If the UI would show the same copy as an existing failure,
reuse that one.

## Gotchas

- `kind` is a hand-written string, not `runtimeType.toString()`:
  minification reduces `runtimeType` to a letter in release builds.
- `failure.message` is for logs only. Never show it to users.
- `UnknownFailure` keeps `error` and `stackTrace`; report from there.
- Never `rethrow`, never `throw` in a repository or data source.
- Do not catch inside the repository to "add context": wrap at the data
  source (`guard`) or add a typed failure.
- Per-endpoint timeout: override `requestTimeout` in the data source.

## Testing

Fake the `ApiClient`, throw an `ApiException`, assert the `Result`:

```dart
test('maps 401 to AuthFailure', () async {
  when(() => api.get(any())).thenThrow(const ApiResponseException(statusCode: 401));
  final result = await dataSource.fetchProfile();
  expect(result.exceptionOrNull(), isA<AuthFailure>());
});
```

## Review checklist

- [ ] No `try/catch` or `throw` in repositories or data sources (outside `guard`)
- [ ] Every repository/data-source method returns `AsyncResult<…>`
- [ ] No `package:result_dart` import in feature code
- [ ] Models are converted to entities before leaving the repository
- [ ] UI strings come from l10n by failure type, not from `failure.message`
- [ ] New failure types are sealed subclasses with a literal `kind`

Files in this skill

  • SKILL.md6.7 KB
  • assets/api_exception.dart2 KB
  • assets/app_failure.dart2.9 KB
  • assets/errors.dart144 B
  • assets/remote_data_source_guard.dart1.5 KB
  • assets/result.dart835 B
  • assets/submission_notifier.dart1.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.