DEV Community

Cover image for Handling State & Errors in Flutter Like a Pro
hassan ghasemzadeh
hassan ghasemzadeh

Posted on

Handling State & Errors in Flutter Like a Pro

Why resultex v3.2.0 is the enterprise-grade result-wrapper you’ve been looking for.
If you’ve been building Flutter apps for a while, you know the drill. Managing asynchronous operations — like fetching data from an API — often leads to a mess of boilerplate. You have to handle isLoading flags, catch exceptions, show SnackBars for errors, and somehow log those errors to Firebase Crashlytics or Sentry without turning your UI code into spaghetti.

We’ve all written code like this at some point:

// The Spaghetti Approach 🍝
try {
  setState(() => isLoading = true);
  final data = await api.fetchData();
  setState(() {
    isLoading = false;
    this.data = data;
  });
} catch (e, stackTrace) {
  setState(() => isLoading = false);
  ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));

  // Polluting UI with telemetry logic
  FirebaseCrashlytics.instance.recordError(e, stackTrace); 
}

Enter fullscreen mode Exit fullscreen mode

It works, but it doesn’t scale. What if you want to implement Stale-While-Revalidate (SWR)? What if you want to auto-retry failed requests?

Enter resultex v4.2.2.

resultex is a production-ready, monadic result-wrapper and state management library for Flutter. In the latest v3.2.0 release, we’ve introduced features that make handling states, side-effects, and global telemetry cleaner than ever.

Let’s dive into what makes it a must-have for your next project.

1. Global Telemetry: Stop Polluting Your UI
Logging errors to third-party services like Firebase Crashlytics, Sentry, or Datadog shouldn’t happen inside your Widgets or Blocs.

With the new ResultexObserver, you can intercept every single Failure globally. Just initialize it once in your main.dart:

`

`
Enter fullscreen mode Exit fullscreen mode

Now, whenever a FailureResult is instantiated anywhere in your app, it automatically reports the error. Your UI layers stay pristine.
2. Seamless UX with Stale-While-Revalidate (SWR)
Users hate looking at blank loading screens. If you already have data on the screen and want to refresh it (e.g., Pull-to-Refresh), you should show the existing data along with a subtle background loading indicator.

Download the Medium app
ResultNotifier handles this out of the box with the isRefreshing flag and the refresh() method:

// Fetch new data in the background without clearing the current UI
await myNotifier.refresh(() => api.fetchUserList());
Enter fullscreen mode Exit fullscreen mode

In your UI, you simply check the state:

ResultBuilder<UserList>(
  notifier: myNotifier,
  onLoading: () => const CircularProgressIndicator(), // Initial full-page load
  onSuccess: (data) => Stack(
    children: [
      UserListView(users: data),
      if (myNotifier.isRefreshing) 
        const LinearProgressIndicator(), // Subtle background refresh
    ],
  ),
);
Enter fullscreen mode Exit fullscreen mode

3. Clean UI Side-Effects (No More Spaghetti SnackBars)
Remember the messy ScaffoldMessenger in our first example? Handling side-effects like showing SnackBars, dialogs, or navigating to another screen shouldn't be mixed with your UI building logic.

resultex provides ResultListener (for pure side-effects) and ResultConsumer (which combines building and listening) to keep your widget tree clean.

Here is how you handle side-effects elegantly without a single try-catch block in your UI:

ResultConsumer<UserList>(
  notifier: myNotifier,
  // 1. Handle side-effects (SnackBars, Dialogs, Routing)
  listener: (context, result) {
    if (result is FailureResult) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(result.failure.message)),
      );
    } else if (result is SuccessResult) {
      // e.g., Navigate to details page on success
    }
  },
  // 2. Build your UI cleanly
  builder: (context, result) {
    if (result == null) return const CircularProgressIndicator();

    return result.when(
      onSuccess: (data) => UserListView(users: data),
      onFailure: (failure) => ErrorView(message: failure.message),
    );
  },
);
Enter fullscreen mode Exit fullscreen mode

This separation of concerns ensures your builders only care about rendering widgets, while your listeners handle the actions.
4. Pre-Built Network Failures
Stop reinventing the wheel for every API integration. resultex v3.2.0 introduces a comprehensive suite of standardized HTTP failures.

Whether you are dealing with a 404 Not Found, a 401 Unauthorized, or a 422 Validation error (perfect for Laravel or NestJS backends), resultex has a built-in class for it:

// Example of structured API error handling
if (response.statusCode == 422) {
  return Result.failure(ValidationFailure(
    message: 'Invalid input',
    errors: response.data['errors'], // Map of field errors
  ));
} else if (response.statusCode == 429) {
  return Result.failure(RateLimitFailure(
    message: 'Too many requests.',
  ));
}
Enter fullscreen mode Exit fullscreen mode
  1. Resilient APIs with Auto-Retry Network instability is a reality of mobile development. Instead of failing immediately, you can use resultex's smart extension to retry failed operations with exponential backoff.
final result = await () => api.fetchData()
    .retryWithBackoff(
      maxAttempts: 3,
      initialDelay: const Duration(seconds: 1),
    );
Enter fullscreen mode Exit fullscreen mode

If the user is on a flaky connection, the package will automatically retry the request before emitting a final Failure.

Wrapping Up
State management doesn’t have to be verbose, and error handling shouldn’t clutter your business logic. By strictly adhering to Dart conventions, resultex provides maximum encapsulation and a genuinely clean codebase.

👉 Add it to your project: flutter pub add resultex

👉 Read the docs on Pub.dev: resultex on Pub.dev

⭐ Don't forget to star the repo on GitHub if you find it helpful! (https://github.com/Hassan-Ghasemzadeh/error_handler/tree/main)

Top comments (0)