Simple error handling in Flutter with Sealed Classes

Rafael Perez
3 min readAug 11, 2023

--

Photo by Michael Dziedzic on Unsplash

Handling errors properly is a must when building an application. Usually, when things don’t work as expected, we want to let know the user that something went wrong in a nice and user-friendly manner. The first step to being able to do this is to provide the application with a way to detect errors and decide what to do with them.

Let’s say we are doing an asynchronous call to get a response from the network, most of the time the app will receive the expected value without further issues, but there is always a chance for that call to fail and throw an exception, maybe because of an unstable internet connection, or a server error, and our app must be able to decide what to do in both cases.

It would be great to have a class that can hold both types, the successful response type and the error type, so we can return this class as the result of the asynchronous call. Dart sealed classes are a great way to achieve this. The sealed modifier makes a class abstract and extendable inside its own library (file) only, so only the classes declared in that library can extend from it or implement it. This lets us create a known inheritance hierarchy, like an enumeration with superpowers.

Now that we understand what a sealed class is, let's create a Result class that can hold a Success when the call returns an actual response or an Error when the call throws an exception.

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

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

class Error<T> extends Result<T> {
final Exception exception;
const Error({required this.exception});
}

Making use of generics, we have defined a Result class that holds values of type T. Since Result is an abstract class, we cannot directly instantiate it, we only can create instances of Success objects and Error objects instead, since they are concrete implementations of Result. The Success class has a data field of type T that will contain the network response we want to receive, while the Error class has a field exception of type Exception that will contain any possible exception we get.

For simplicity, let’s say we are making the network call directly inside a repository. We can make use of our Result class like this:

Future<Result<Data>> asynchronousCall() async {
Uri url = Uri.https(baseUrl, endpoint);
try {
http.Response response = await http.get(url);
if(response.statusCode == 200) {
Data data = dataFromJson(response.body);
return Success(data: data);
} else {
throw Exception(response.reasonPhrase);
}
} on Exception catch(exception) {
return Error(exception: exception);
}
}

If we receive an actual response from the network, we return a Success object containing the data we want, and if we get an exception we return an Error object that contains that exception.

Now we can handle the Result to update the UI accordingly. We could have something like this in our viewmodel:

getUiData() async {
Result<Data> result = await repository.asynchronousCall();
if(result is Success) {
_uiData = (result as Success<Data>).data;
_exception = null;
} else {
_exception = (result as Error).exception;
}
notifyListeners();
}

In case we get a Success result we assign the data to a _uiData field that represents our UI state. If we get an Error result instead, we can assign the exception to an _exception field that we can manage in our UI.

This was a simple example of how we can use sealed classes in Dart to handle errors with ease. We could make some adjustments, though, like adding a generic type E that extends from the Exception class, or any other adjustment we want depending on the application needs.

Hope this article has been helpful for you. See you in another story.

--

--

Rafael Perez
Rafael Perez

Written by Rafael Perez

Mobile developer. I’m interested on everything related to the life cycle of an app, design, development, and management.

Responses (3)