Best Practices for Error Handling

Learn how to handle errors in Dart with try-catch and return values.

Flutter Logo with error handling as the text

Today we will cover the infamous debate of try-catch vs return values.

Should I use try-catch to throw an exception or return an error to the function? It’s one of those debates that can divide a room of developers.

Try catch vs return values

Let’s go through an overview of both and explain which one we choose.

Try-catch

Let’s start with try-catch.

When you are calling an external API with HttpClient, you will commonly see an exception called SocketException. If the fetchData function needs to return a string, we should use a try-catch and specifically catch the SocketException to inform the calling function that there was a network issue.

Future<String> fetchData(String url) async {
final HttpClient client = HttpClient();
try {
// Create the request
final HttpClientRequest request = await client.getUrl(Uri.parse(url));
// Send the request and wait for a response
final HttpClientResponse response = await request.close();
// Read and return the response body
if (response.statusCode == 200) {
return await response.transform(const Utf8Decoder()).join();
} else {
throw HttpException('Failed with status code: ${response.statusCode}');
}
} on SocketException catch (e) {
return 'Error: Unable to fetch data due to a network issue.';
} finally {
client.close();
}
}

This might seem like a small thing, but it’s important. If we propagate this message to the user, they can check to make sure they are connected to the internet. If we didn’t do this, they would get a general error and have no idea what’s going on.

Return value

Now, return values are a bit different. The previous HTTP scenario doesn’t work because Dart doesn’t have built-in return values to handle errors.

Return value error handling is not built into the language like it is in other languages such as Rust.

Rust logo

But you can create your own return value handling.

First, we have to define what types of returns we should have. In this case, we have two subclasses named Ok and Error. This code is directly taken from the Flutter architecture documentation.

sealed class Result<T> {
const Result();
/// Creates an instance of Result containing a value
factory Result.ok(T value) => Ok(value);
/// Create an instance of Result containing an error
factory Result.error(Exception error) => Error(error);
}
/// Subclass of Result for values
final class Ok<T> extends Result<T> {
const Ok(this.value);
/// Returned value in result
final T value;
}
/// Subclass of Result for errors
final class Error<T> extends Result<T> {
const Error(this.error);
/// Returned error in result
final Exception error;
}

Here is a really simple function using return values. If you get an error, you return an error type with a message. If you don’t have an error, you would return an OK type with a message.

Result<String> someFunction(bool returnError) {
if(returnError) {
return Result.error('opsi')
}
return Result.ok('woo')
}

Then you would handle the return with a switch case to enforce handling of the error. Now you’re forced to handle this error within your code, unlike try-catch.

void callSomeFunction() {
final result = someFunction()
switch (result) {
case Ok<String>():
// do something
case Error<String>():
// explicitly have to handle the error
}
}

There are quite a lot of packages that already have result-based classes implemented, such asmultiple_resultorresult_dart. Using one of these package can simplify the process of having multiple types of return values.

Multiple result package

Result dart package

Pros and Cons

There are pros and cons with both. You could argue, since a result-based system is not built into Dart, means you shouldn’t use it. Others argue that the benefits of return-based error handling make error handling explicit, which leads to fewer side effects in your code.

The main downside is the lack of chaining for return-based error handling. You can quickly run into deep nesting of functions and their return types.

void callSomeFunction() {
final result = someFunction()
switch (result) {
case Ok<String>():
final result = someOtherFunction()
switch (result) {
case Ok<String>():
// do something
case Error<String>():
// explicitly have to handle the error
}
case Error<String>():
// explicitly have to handle the error
}
}

Opt to use try-catch when dealing with APIs. If there is a need for chaining errors, this is the place you are likely to do it.

Architecture highlighting where to use try-catch

Lower down in the stack, for example in view-models, avoid using try-catch for error handling. We want to explicitly enforce handling of any potential issue. If any issues arise here, either notify the user or handle it.

Architecture highlighting where to use return values

It depends…

Like always in software, the correct answer of when to use try-catch vs return values is that it depends on the situation.

A good approach is to start with try-catch and introduce result-based error handling as you see fit.

Thank you for reading.

If you want to learn how to build production-ready Flutter apps, check out the Best Flutter Course below.

Course thumbnail

Want to learn Flutter?

We teach you all you need to confidently build apps at hungrimind.com/learn/flutter

YouTube Video

Get articles right in your inbox

No spam, unsubscribe anytime. We treat your inbox with respect.