The Universe of Exceptions in Dart: An Uncomplicated Guide
Tags: beginners, dart, learning, flutter
Published: 2023-12-07 21:08:43

Developing in Dart is an exciting journey full of challenges, and one of the crucial aspects of this journey is exception handling. In this article, we'll dive head first into these topics, demystifying exception handling to make it more accessible for our end users.

The importance of implementing exception handling is to be able to deal with situations that we cannot always predict, so that our users are not impacted. From this we were able to build more robust applications, capable of dealing with unexpected situations effectively.

## **Success Only Programming**

A common approach in the world of programming is to program a solution targeting only the path that works, where everything always works out. However, by thinking only about success, we end up not preparing our application to deal with real-world situations, where things may not work the way we expect. By neglecting these problems, we end up with a highly unpredictable and low-quality application.

It is important to think beyond the success story, and prepare the system for extraordinary, exceptional situations.

## **Let's understand the difference between Errors and Exceptions**

In the Dart documentation, we can understand in more depth about [Error Handling](https://dart.dev/language/error-handling#exceptions) and some definitions that we will cover now, but in order to be objective:

### **Exceptions**

Situations that can be anticipated and that the software must be prepared to deal with.

### **Errors**

Situations that should not occur and that the developer should have avoided.

## **Try On-Catch-Finally: Dealing with the Unexpected**

Now, how can we anticipate and deal with exceptional situations? Within Dart, and many other programming languages, we have a code structure precisely designed to help us deal with this: **Try On-Catch-Finally**.

### **Try**

`Try` is used when we have a block of code that we know can cause a failure. So, we wrap the code with the `Try` structure, so that it can be properly handled.

In the example below, we can see that the `toInt()` method was structured with only the happy path in mind, however, if an error occurs, the system will simply ‘crash’.

```dart
void main() => print(toInt("5"));

int toInt(String value) {
   return int.parse(value); //Works perfectly
}
```

```dart
void main() => print(toInt("not a number"));

int toInt(String value) {
   return int.parse(value); //Uncaught Error: FormatException: not a number
}
```

**How can we prepare the system to deal with this situation?**

Using the `Try` structure.

```dart
void main() {
   try {
     print(toInt("not a number"));
   } // we'll need to complete this with 'catch', 'on' or 'finally'
}

int toInt(String value) {
   return int.parse(value);
}
```

### **On**

Now that we purposely caused an exception, and started the process of handling it so that our user is not impacted, let's understand how to handle it when we know exactly the exception that will be thrown.

We know that if we pass a String `“not a number”`, the `toInt()` method will throw a `FormatException`. To be able to catch this exception, we will use the **on** structure.

`on` is ideal for use in situations where we know which exception can be triggered or when we want to define special treatment for a certain type of exception.

```dart
void main() {
   try {
     print(toInt("not a number"));
   } on FormatException {
     print("The value is not a valid number.");
   }
}

int toInt(String value) {
   return int.parse(value);
}
```

### **Catch**

Using `catch` is another way to capture an exception.

Catching an exception means that it will not be propagated further. It is an opportunity to address it so that the user understands what went wrong.

```dart
void main() {
   try {
     print(toInt("not a number"));
   } on FormatException catch (e) {
     print(e.toString());
   }
}

int toInt(String value) {
   return int.parse(value);
}
```

`catch` provides the parameter `e` which is equivalent to `error` or `exception`. With it, we have access to the triggered object that can help us understand what went wrong.

<aside>
💡 If the captured object is a FormatException, ‘e’ is of type FormatException.

</aside>

It is important to highlight that in Dart, anything can be thrown as an `exception`, so it is important to know how they work in order to handle them in the best possible way.

### **Finally**

`finally` is a clause that guarantees that a given piece of code is executed regardless of whether an exception is triggered or not. If an exception is thrown, it is only triggered after.

```dart
void main() {
  try {
    print(toInt("not a number")); // 1º
  } on FormatException catch (e) {
    print(e.toString()); // 2º
  } finally {
    print("we reached the end"); // 3º
  }
}

int toInt(String value) {
  return int.parse(value);
}
```

```dart
we reached the end
Uncaught Error: FormatException: not a number
```
## **Throwing Exceptions with Throw**

A very common situation is when we try to indicate the success or failure of an operation using booleans, or returning some value that represents success or error. But in cases like this, when an error is returned, finding its exact source can be like looking for a needle in a haystack.

Below, we can see that the `toInt()` method does not give us any information if something exceptional happens.

For example, I created the rule that **the passed String cannot be empty**. However, the only way I can check is if the `toInt()` method returns `0`.

This ends up being a problem when I inform, for example, `toInt("0")`. How do I know when the result returned is a success or error `0`?

```dart
void main() {
    print(toInt("0")); // return 0 - success
    print(toInt("")); // return 0 - error
}

int toInt(String value) {
   if (value.isEmpty) return 0; // <- indicates an error
  
   return int.parse(value); // <- indicates success
}
```

To facilitate this process, let's understand the concept of throwing exceptions with `throw`. This practice will help us improve the clarity of our codes and also provide a clear path to understanding and solving problems, without the uncertainty of abstract values.

```dart
void main() {
   try {
     print(toInt(""));
   } on Exception catch (e) {
     print(e.toString());
   }
}

int toInt(String value) {
   if (value.isEmpty) {
     throw Exception("The provided value is empty.");
   }

   return int.parse(value);
}
```

With the adjustment we made above, we can validate within the method itself that **the String passed cannot be empty**, and if it is, an `exception` will be thrown informing the user that the rule was not met. Otherwise, the method continues normally.

It is also important to take into account that `int.parse(value)` can still trigger any type of exception. Therefore, we have our `try {} on Exception catch (e) {}` structure in the `main` method.

## **Creating Custom Exceptions**

To further improve the clarity of our code, and the projects we work on in our daily lives, we can create customized exceptions.

I will create an exception for each rule:

- The value provided can't be empty;
- The value provided can't be negative.

```dart
class InformedValueIsEmptyException implements Exception {
   @override
   String toString() => 'The provided value is empty.';
}

class InformedValueIsUnderLimitException implements Exception {
   String value;

   InformedValueIsUnderLimitException({required this.value});

   @override
   String toString() => "The provided value can't be less than 0. You entered '$value'.";
}

```

By organizing our `toInt()` method with the appropriate exceptions being referenced, we can clearly see the possible situations we have and how we can handle them clearly.

```dart
int toInt(String value) {
   if (value.isEmpty) throw InformedValueIsEmptyException();

   int result = int.parse(value);

   if (result < 0) throw InformedValueIsUnderLimitException(value: value);

   return result;
}
```

### **Properties for Exceptions**

Sometimes, as an exception, it is necessary to provide the user with information that may be useful so that they can have a better understanding of the problem. For example, in the exception below, we make it clear to the user that the value **x** they provided don't satisfy our rule that this value can't be negative.

<aside>
💡 To identify the necessary parameters, think about how to clearly communicate the information to the user.

</aside>

```dart
class InformedValueIsUnderLimitException implements Exception {
String value;

InformedValueIsUnderLimitException({required this.value});

@override
String toString() => "The provided value can't be less than 0. You entered '$value'.";
}
```

## Saving in memory

Throughout this article, we explored the world of exception handling in Dart. Understanding how to implement exception handling strengthens our applications, preparing them to effectively deal with unexpected situations.

When programming, it's common to only focus on everything working perfectly. It's important not to neglect preparing for real-world situations. The best possible approach involves thinking beyond the success case, preparing the system to deal with exceptional situations.

Dart's **Try On-Catch-Finally** structure is a powerful tool for dealing with the unexpected. Using `try` to wrap blocks of code that may fail, `on` to catch specific exceptions, `catch` to handle exceptions and `finally` to guarantee code execution, even after an exception occurs.

We learned about throwing exceptions with `throw`, abandoning the common practice of indicating success or failure using booleans or special values. We also created custom exceptions, such as `InformedValueIsEmptyException` and `InformedValueIsUnderLimitException`, that helped provide clarity about the problems we encountered, making our code easier to understand and maintain.

In short, by adopting good exception handling practices and creating custom exceptions, we increase the quality of our code, promoting more robust, secure and understandable applications.

<aside>
💡 This article was produced based on notes from my studies on the Alura course [Dart: Dealing with Exceptions and Null Safety](https://www.alura.com.br/curso-online-dart-lidando-exceptions-null-safety).

</aside>