SuperAPI AI Directory

flutter-error-handling

General & other

View repository

Updated 10-15 23:42

About this skill

Flutter error handling — Result<D, E>

Core philosophy

For predictable failures, do not throw exceptions. Instead, return Result expressed as a type. This forces callers to handle failure cases through the type system, eliminating UI error paths that are missed at runtime.

Exceptions are only used to catch what the framework/platform throwsat the lowest layer (Data) and convert it to Result.error(...) . UseCase, ViewModel, and Screen no longer see try/catch.


Base types (lib/core/domain/error/)

Error marker interface

// lib/core/domain/error/error.dart
abstract interface class Error {}

Every custom error type implements this Error . Result 의 E must be extends Error , so it does not mix with the Dart standard Exception .

Result<D, E>

// lib/core/domain/error/result.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'error.dart';

part 'result.freezed.dart';

@freezed
sealed class Result<D, E extends Error> with _$Result<D, E> {
  const factory Result.success(D data) = ResultSuccess;
  const factory Result.error(E error) = ResultError;
}

Important: sealed Because it is declared as switch , the Dart compiler enforces all cases in


. If you miss a case, a compile warning appears.

Defining feature-specific errors enum + implements Error Errors are defined as toString() . Each value places the Korean message to show the user in

// lib/core/domain/error/network_error.dart
enum NetworkError implements Error {
  requestTimeout,
  noInternet,
  serverError,
  unknown;

  @override
  String toString() => switch (this) {
        NetworkError.requestTimeout => '요청 시간이 초과되었습니다',
        NetworkError.noInternet => '인터넷 연결을 확인해 주세요',
        NetworkError.serverError => '서버에 문제가 발생했습니다',
        NetworkError.unknown => '알 수 없는 문제가 발생했습니다',
      };
}

.NetworkError)는 lib/core/domain/error/ Shared errors go in lib/domain/error/<feature>_error.dart , and feature-specific errors go in BookmarkError, NewRecipeError.

. Example: 한 Result Multiple errors are not represented.


contains exactly one error. If you need to communicate multiple conditions at once, that is a domain design problem.

// UseCase 시그니처
Future<Result<List<String>, NetworkError>> execute();
Future<Result<List<Recipe>, BookmarkError>> execute(int recipeId);
  • UseCase / Repository return types D Specify the success data type E and the error type
  • . NetworkError.unknown In the Data layer, catch network/DB exceptions and convert them to values such as
  • . UseCase maps multiple Repository errors to its own feature error and returns it (e.g., when bookmark saving fails,). BookmarkError.saveFailed).

Consuming in ViewModel

Since it is a sealed type, specify the type parameters and switch pattern match inhome_view_model.dart . This project uses the following form as the standard (see

void _fetchCategories() async {
  final result = await _getCategoriesUseCase.execute();

  switch (result) {
    case ResultSuccess<List<String>, NetworkError>():
      _state = state.copyWith(
        categories: result.data,
        selectedCategory: 'All',
      );
      notifyListeners();

    case ResultError<List<String>, NetworkError>():
      switch (result.error) {
        case NetworkError.requestTimeout:
        case NetworkError.noInternet:
        case NetworkError.serverError:
        case NetworkError.unknown:
          _eventController.add(result.error);
      }
  }
}

).:

  • ResultSuccess<D, E>() / ResultError<D, E>() Why use it this way result.data / result.error You must write
  • to preserve generics and keep the concrete type of switch (result.error) alive.

The inner

forces all enum cases to be listed, so when a new error is added, the compiler tells you the missing handling points.

How to deliver errors to the UI StreamController There are two standard patterns.

final _eventController = StreamController<NetworkError>();
Stream<NetworkError> get eventStream => _eventController.stream;

1) A snackbar/toast shown once — eventStream event ScaffoldMessenger.showSnackBar is called. Putting it in state causes it to repeat on every rebuild, so it is emitted as an event.

2) Persistent state (error banner) — State field

If the error screen itself must be drawn, State 에 NetworkError? error place a field and copyWith(error: ...) reflect it with . When the user closes or retries, error: null reset it with .


Data layer — the point where exceptions are converted to Result

Future<Result<List<RecipeDto>, NetworkError>> getRecipes() async {
  try {
    final raw = await _recipeDataSource.getRecipes();
    return Result.success(raw.map(RecipeDto.fromJson).toList());
  } on SocketException {
    return const Result.error(NetworkError.noInternet);
  } on TimeoutException {
    return const Result.error(NetworkError.requestTimeout);
  } catch (_) {
    return const Result.error(NetworkError.unknown);
  }
}

Principle:

  • The layer where the exception occurs is the layer that catches it. Platform/HTTP exceptions are converted in Data, and domain validation failures in Domain to Result.error .
  • Ensure that exceptions never rise up to Presentation. In the ViewModel, try/catch appearing is a warning sign.

Decision table for which error type to use

Scenario Error type Location
Network call failure NetworkError core/domain/error/
Local storage/DB failure LocalError (new if needed) core/domain/error/
Feature-specific failure (e.g., bookmark save failure) BookmarkError same enum lib/domain/error/
Repository that groups multiple DataSources Parent error type (NetworkError or feature error) the corresponding feature

Checklist

  • The failure types of the new feature are defined as enum + implements Error defined as
  • The UseCase signature explicitly specifies the type parameter as Future<Result<D, FooError>> specifies the type parameter as
  • In the ViewModel, switch (result) 가 ResultSuccess<D, E>() / ResultError<D, E>() handle all of them
  • All values of the error enum are enumerated in the internal switch (result.error) are listed in
  • The Data layer catches platform exceptions and converts them Result.error converts to
  • Nowhere in Presentation does try/catch float around

Anti-pattern

  • ❌ Future<List<Recipe>> Return as is and pass the failure throw → the caller forgets the failure.
  • ❌ Result<List<Recipe>, Exception> Like , Exception using as the error type → Error the meaning of the marker disappears.
  • ❌ 한 Result Putting multiple errors in a list in → the model becomes complex and UI branching explodes.
  • ❌ switch In , default: lumping together with → throws away the safety net where the compiler warns when a new error value is added.