Flutter Error Handling and Exceptions

Things go wrong in real apps. Networks drop, servers fail, files vanish, and users type unexpected text. A well-built app expects these problems and responds with calm, helpful messages instead of a crash. Error handling is the skill of catching problems early, deciding what they mean, and telling the user what to do next.

Errors and Exceptions

Dart separates two kinds of problems. An Exception describes a situation a program can predict and recover from, such as a failed network call. An Error describes a programming mistake, such as reading beyond the end of a list. Catch exceptions. Fix errors in the code.

try, catch and finally


Future<void> loadProfile() async {
  try {
    final data = await fetchProfile();
    print('Loaded: $data');
  } catch (e) {
    print('Something went wrong: $e');
  } finally {
    print('Loading finished');
  }
}

  try block runs
     │
     ├── no problem ──→ skip catch ──┐
     │                               ▼
     └── problem ────→ catch block ─→ finally block (always runs)

The finally block always runs, which makes it the right place to hide a loading spinner.

Catching Specific Types

Use on to react differently to different problems.


try {
  int age = int.parse(input);
} on FormatException {
  print('Please enter digits only');
} on Exception catch (e) {
  print('Unexpected problem: $e');
}

Order matters. List narrow types first and broad types last.

Throwing Your Own Exceptions

Custom exception classes give problems clear names and carry useful details.


class NoInternetException implements Exception {
  final String message;
  NoInternetException([this.message = 'No internet connection']);
}

class ServerException implements Exception {
  final int statusCode;
  ServerException(this.statusCode);
}

void check(bool online) {
  if (!online) {
    throw NoInternetException();
  }
}

Handling Dio Errors

Dio wraps network problems in a DioException with a type property.


String friendlyMessage(DioException e) {
  switch (e.type) {
    case DioExceptionType.connectionTimeout:
    case DioExceptionType.receiveTimeout:
      return 'The server is taking too long. Try again.';
    case DioExceptionType.connectionError:
      return 'Check your internet connection.';
    case DioExceptionType.badResponse:
      return 'Server problem (code ${e.response?.statusCode}).';
    default:
      return 'Something went wrong.';
  }
}

Raw messages such as "SocketException: Failed host lookup" confuse users. Translate each failure into one plain sentence and a clear next step.

Showing Errors in the Interface

Every screen that loads data needs three states: loading, success, and failure. The failure state should explain the problem and offer a retry button.


  ┌──────────────────────────┐
  │        (icon)            │
  │  Could not load posts    │
  │  Check your connection   │
  │       [ Try again ]      │
  └──────────────────────────┘

The Result Pattern

Instead of throwing exceptions across many layers, a function can return an object that holds either a value or a failure. Callers must handle both cases.


sealed class Result<T> {}

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

class Failure<T> extends Result<T> {
  final String message;
  Failure(this.message);
}

Future<Result<String>> getName() async {
  try {
    return Success('Asha');
  } catch (e) {
    return Failure('Could not load name');
  }
}

final result = await getName();
switch (result) {
  case Success(:final value):
    print(value);
  case Failure(:final message):
    print(message);
}

The compiler checks that every case receives handling, which prevents forgotten failures.

Catching Everything Else

Flutter provides global hooks for errors that escape local handling.


void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    print('Framework error: ${details.exception}');
  };

  PlatformDispatcher.instance.onError = (error, stack) {
    print('Uncaught error: $error');
    return true;
  };

  runApp(MyApp());
}

Send these reports to a crash reporting service, such as Firebase Crashlytics, so you learn about problems that users never report.

Replacing the Red Error Screen

Release builds show a plain gray box when a widget fails to build. Set ErrorWidget.builder to show a friendlier message.

Good Habits

  • Catch the narrowest exception type that makes sense.
  • Log the technical detail for developers and show a gentle message to users.
  • Never print passwords, tokens, or personal data in logs.
  • Offer a retry action wherever a repeat attempt might succeed.

Practice Ideas

Write a function that parses a number from user text and returns a Result. Build a screen that shows loading, success, and failure states for a network call, and test the failure state with airplane mode on.

Writing Messages People Understand

A good error message answers three questions: what happened, why it matters, and what to do now. "Upload failed" leaves people stuck. "The photo could not upload because the connection dropped. Try again when you are back online" gives a clear path forward. Avoid blame, avoid technical jargon, and avoid exclamation marks. A calm tone keeps trust high even on bad days.

Reporting Errors to a Service

Users rarely report crashes. They close the app and move on. A crash reporting service collects error details, device models, and app versions automatically, and groups similar reports together. Review the grouped list weekly, fix the most frequent problem first, and check the next release for a drop in that count. This loop turns invisible failures into a steady improvement plan.