DEV Community

Mabel
Mabel

Posted on

The Universe of Exceptions in Dart: An Uncomplicated Guide

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 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’.

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

int toInt(String value) {
   return int.parse(value); //Works perfectly
}
Enter fullscreen mode Exit fullscreen mode
void main() => print(toInt("not a number"));

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

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

Using the Try structure.

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

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

int toInt(String value) {
   return int.parse(value);
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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);
}
Enter fullscreen mode Exit fullscreen mode
we reached the end
Uncaught Error: FormatException: not a number
Enter fullscreen mode Exit fullscreen mode

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?

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
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
}
Enter fullscreen mode Exit fullscreen mode

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.
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'.";
}

Enter fullscreen mode Exit fullscreen mode

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.

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

   int result = int.parse(value);

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

   return result;
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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'.";
}
Enter fullscreen mode Exit fullscreen mode

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.

💡 This article was produced based on notes from my studies on the Alura course Dart: Dealing with Exceptions and Null Safety.

Top comments (0)