← Articles

Error handling strategies in Flutter

By Charlin Joe · 8 October 2025

Error handling in Flutter spans three contexts: synchronous code (exceptions), async code (Future errors), and stream errors. Handling each consistently requires a strategy — not scattered try/catch blocks throughout the codebase.

The error taxonomy

Distinguish between errors types:

  • Expected errors: network failures, not found, permission denied. Handle explicitly and show user-friendly messages.
  • Programming errors: null pointer, index out of bounds, assertion failures. These are bugs — crash in debug, log in release.
  • Flutter errors: widget build failures, layout overflows. Handled by FlutterError.onError.

Result type pattern

For expected errors at the repository layer, return a result type instead of throwing:

sealed class Result<T> {
  const Result();
}

final class Success<T> extends Result<T> {
  const Success(this.value);
  final T value;
}

final class Failure<T> extends Result<T> {
  const Failure(this.error);
  final AppError error;
}

// Repository
Future<Result<Order>> getOrder(String id) async {
  try {
    final doc = await _db.collection('orders').doc(id).get();
    if (!doc.exists) return Failure(NotFoundError('Order $id not found'));
    return Success(Order.fromFirestore(doc));
  } on FirebaseException catch (e) {
    return Failure(NetworkError(e.message ?? 'Database error'));
  }
}

// Usage
final result = await repo.getOrder(orderId);
switch (result) {
  case Success(:final value):
    displayOrder(value);
  case Failure(:final error):
    showError(error.userMessage);
}

Global error catching

Set up error handlers in main() before runApp:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  // Flutter framework errors (widget build, layout)
  FlutterError.onError = (details) {
    if (kDebugMode) {
      FlutterError.presentError(details); // Print to console in debug
    } else {
      FirebaseCrashlytics.instance.recordFlutterFatalError(details);
    }
  };

  // Async errors not caught by the Flutter framework
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true; // Return true to prevent the error propagating further
  };

  runApp(const MyApp());
}

Error boundary widget

Catch errors in a subtree to prevent full-screen crashes:

class ErrorBoundary extends StatefulWidget {
  const ErrorBoundary({super.key, required this.child, this.fallback});
  final Widget child;
  final Widget? fallback;

  @override
  State<ErrorBoundary> createState() => _ErrorBoundaryState();
}

class _ErrorBoundaryState extends State<ErrorBoundary> {
  Object? _error;

  @override
  void initState() {
    super.initState();
    // Catch errors thrown during build
  }

  @override
  Widget build(BuildContext context) {
    if (_error != null) {
      return widget.fallback ?? const Center(child: Text('Something went wrong'));
    }
    return widget.child;
  }
}

// Use ErrorWidget.builder for build-time errors
ErrorWidget.builder = (FlutterErrorDetails details) {
  return Scaffold(
    body: Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(Icons.error_outline, color: Colors.red, size: 48),
          const SizedBox(height: 16),
          Text(kDebugMode ? details.exceptionAsString() : 'Something went wrong'),
        ],
      ),
    ),
  );
};

BLoC error handling

Future<void> _onLoadOrder(LoadOrder event, Emitter emit) async {
  emit(const OrderLoading());
  try {
    final order = await _repo.getOrder(event.id);
    emit(OrderLoaded(order));
  } on NotFoundException {
    emit(const OrderError(message: 'Order not found'));
  } on NetworkException catch (e) {
    emit(OrderError(message: e.userMessage));
  } catch (e, stack) {
    // Unexpected error — log and show generic message
    await Sentry.captureException(e, stackTrace: stack);
    emit(const OrderError(message: 'Something went wrong. Please try again.'));
  }
}

Retry with exponential backoff

Future<T> withRetry<T>(
  Future<T> Function() fn, {
  int maxAttempts = 3,
  bool Function(Object e)? shouldRetry,
}) async {
  for (int i = 0; i < maxAttempts; i++) {
    try {
      return await fn();
    } catch (e) {
      final isRetryable = shouldRetry?.call(e) ?? _isTransientError(e);
      if (!isRetryable || i == maxAttempts - 1) rethrow;
      await Future.delayed(Duration(milliseconds: 200 * (i + 1)));
    }
  }
  throw StateError('Should not reach here');
}

bool _isTransientError(Object e) {
  if (e is FirebaseException) {
    return ['unavailable', 'deadline-exceeded'].contains(e.code);
  }
  return false;
}

// Usage
final orders = await withRetry(() => repo.getOrders());

Stream error handling

// Handle stream errors without killing the stream
repo.watchOrders().handleError((error, stack) {
  logger.error('Order stream error', error, stack);
  // Stream continues — only this emission is an error
}).listen((orders) {
  // process orders
});

// In StreamBuilder:
StreamBuilder<List<Order>>(
  stream: repo.watchOrders(),
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return ErrorView(
        message: snapshot.error.toString(),
        onRetry: () => setState(() {}), // Rebuilds the StreamBuilder
      );
    }
    if (!snapshot.hasData) return const CircularProgressIndicator();
    return OrdersList(orders: snapshot.data!);
  },
)

User-facing error messages

Never show technical error details to users:

abstract class AppError {
  String get userMessage; // Safe for display
  String get debugMessage; // For logs only
}

class NetworkError extends AppError {
  const NetworkError(this.debugMessage);
  @override final String debugMessage;
  @override String get userMessage => 'Check your internet connection and try again.';
}

Common pitfalls

Empty catch blocks. catch (e) {} swallows errors silently. At minimum, log before suppressing.

Catching Error subclasses. In Dart, Error (not Exception) represents programming errors like AssertionError, StackOverflowError. Catching these prevents the crash that would alert you to the bug. Only catch Exception for expected errors.

Showing different messages in debug vs release. In release builds, show generic user-friendly messages. In debug builds, show the actual error. Never show stack traces to end users.

Sign in to like, dislike, or report.

Error handling strategies in Flutter — ANN Tech