Chapters

Hide chapters

Dart Apprentice: Beyond the Basics

First Edition · Flutter · Dart 2.18 · VS Code 1.71

Dart Apprentice: Beyond the Basics

Section 1: 15 chapters
Show chapters Hide chapters

10. Error Handling
Written by Jonathan Sande

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

It’s natural to only code the happy path as you begin to work on a project.

  • “This text input will always be a valid email address.”
  • “The internet is always connected.”
  • “That function’s return value is always a positive integer.”

Until it isn’t.

When a baker makes a mistake, cookies get burned. When an author makes a mistake, words get mispelled. Eaters will swallow and readers overlook, but code doesn’t forgive. Maybe you noticed misspelled was “mispelled” in the previous sentence. Or maybe you missed it. We all make mistakes, but when a programmer makes one, the whole app crashes. That’s the nature of a programmer’s work. The purpose of this chapter is to teach you how to crash a little less.

Errors and Exceptions in Dart

The creators of Dart had such confidence you and your users would make mistakes that they built error handling right into the language. Before you learn how to handle errors, though, you get to make them!

How to Crash an App

You have many ways to cause your application to give up all hope of survival and quit. Open a new project and try out a few of them.

Dividing by Zero

One way to crash is to divide by zero. You learned in elementary school that you can’t do that.

1 ~/ 0;

Unhandled exception:
IntegerDivisionByZeroException

No Such Method

In the past, you often got a NoSuchMethodError when you forgot to handle null values. Because the Null class doesn’t have many methods, almost anything you tried to do with null caused this crash.

dynamic x = null;
print(x.isEven);
Unhandled exception:
NoSuchMethodError: The getter 'isEven' was called on null.
int x = null;
print(x.isEven);

A value of type 'Null' can't be assigned to a variable of type 'int'.

Format Exception

Another way to crash your app is to try and decode a “JSON” string that isn’t actually in JSON format.

import 'dart:convert';

void main() {
  const malformedJson = 'abc';
  jsonDecode(malformedJson);
}
Unhandled exception:
FormatException: Unexpected character (at character 1)
abc
^
int.parse('42');    // OK
int.parse('hello'); // FormatException
Unhandled exception:
FormatException: Invalid radix-10 number (at character 1)
hello
^

Reading Stack Traces

The sections above only gave you part of the error messages. You probably noticed that the full message was much longer and a lot of it looked like unintelligible gibberish. The unintelligible part is called a stack trace:

Stack trace
Nfajt bkono

void main() {
  functionOne();
}

void functionOne() {
  functionTwo();
}

void functionTwo() {
  functionThree();
}

void functionThree() {
  int.parse('hello');
}
facfmiazJtkae zihxbuahCgu buqdyaabOsi vius
Cebq philf

Stack trace
Dqayz jfiya

Debugging

It’s not always obvious from the stack trace where the bug in your code is. VS Code has good debugging tools to help you out in this situation.

Writing Some Buggy Code

Replace your project file with the following code:

void main() {
  final characters = ' abcdefghijklmnopqrstuvwxyz';
  final data = [4, 1, 18, 20, 0, 9, 19, 0, 6, 21, 14, 27];
  final buffer = StringBuffer();
  for (final index in data) {
    final letter = characters[index];
    buffer.write(letter);
  }
  print(buffer);
}

final letter = characters[index];

Adding a Breakpoint

Click the margin to the left of line 2. This will add a red dot:

Breakpoint on line 2
Rgookwaayn al soqa 4

Running in Debug Mode

Now, start your app in debug mode. Like before, there are a few ways to do that:

Stepping Over the Code Line by Line

VS Code pauses execution at line 2. Then, a floating button bar pops up with various debugging options. If you hover your mouse over each button, you can see what it does. The most important ones for now are the two on the left:

Run and Debug panel: Variables
Zeb onb Rulol fadod: Qeyioqrat

Watching Expressions

When you tire of stepping one line at a time through the for loop, add a breakpoint to line 6: final letter = characters[index];.

Adding a Watch expression
Exwojw u Kepws ogjfopjaax

Fixing the Bug

When the app finally crashes, what’s the value of index?

final characters = ' abcdefghijklmnopqrstuvwxyz!';
dart is fun!

Handling Exceptions

The bug in the last section was an actual error in the logic of the code. There was no way to handle that except to track down the bug and fix it.

Catching Exceptions

As you saw earlier, one source of these exceptions is invalid JSON. When connected to the internet, you can’t control what comes to you from the outside world. Invalid JSON doesn’t happen very often, but you should write code to deal with it when you get it.

import 'dart:convert';

void main() {
  const json = 'abc';

  try {
    dynamic result = jsonDecode(json);
    print(result);
  } catch (e) {
    print('There was an error.');
    print(e);
  }
}
There was an error.
FormatException: Unexpected character (at character 1)
abc
^
const json = 'abc';
const json = '{"name":"bob"}';
{name: bob}

Handling Specific Exceptions

Using a catch block will catch every exception that happens in the try block.

const json = 'abc';

try {
  dynamic result = jsonDecode(json);
  print(result);
} on FormatException {
  print('The JSON string was invalid.');
}
The JSON string was invalid.

Handling Multiple Exceptions

When there’s more than one potential exception that could occur, you can use multiple on blocks to target them.

void main() {
  const numberStrings = ["42", "hello"];

  try {
    for (final numberString in numberStrings) {
      final number = int.parse(numberString);
      print(number ~/ 0);
    }
  } on FormatException {
    handleFormatException();
  } on UnsupportedError {
    handleDivisionByZero();
  }
}

void handleFormatException() {
  print("You tried to parse a non-numeric string.");
}

void handleDivisionByZero() {
  print("You can't divide by zero.");
}
You can't divide by zero.

The Finally Block

There’s also a finally block you can add to your try-catch structure. The code in that block runs both if the try block is successful and if the catch or on block catches an exception.

void main() {
  final database = FakeDatabase();
  database.open();

  try {
    final data = database.fetchData();
    final number = int.parse(data);
    print('The number is $number.');
  } on FormatException {
    print("Dart didn't recognize that as a number.");
  } finally {
    database.close();
  }
}

class FakeDatabase {
  void open() => print('Opening the database.');
  void close() => print('Closing the database.');
  String fetchData() => 'forty-two';
}
Opening the database.
Dart didn't recognize that as a number.
Closing the database.
Opening the database.
The number is 42.
Closing the database.

Writing Custom Exceptions

You should use the standard exceptions whenever you can, but you can also define your own exceptions when appropriate.

Defining the Exceptions

First, create the following Exception class for passwords that are too short:

class ShortPasswordException implements Exception {
  ShortPasswordException(this.message);
  final String message;
}
class NoNumberException implements Exception {
  NoNumberException(this.message);
  final String message;
}

class NoUppercaseException implements Exception {
  NoUppercaseException(this.message);
  final String message;
}

class NoLowercaseException implements Exception {
  NoLowercaseException(this.message);
  final String message;
}

Throwing Exceptions

Now, add the following validation function below main:

void validateLength(String password) {
  final goodLength = RegExp(r'.{12,}');
  if (!password.contains(goodLength)) {
    throw ShortPasswordException('Password must be at least 12 characters!');
  }
}
throw 'rotten tomatoes';
void validateLowercase(String password) {
  final lowercase = RegExp(r'[a-z]');
  if (!password.contains(lowercase)) {
    throw NoLowercaseException('Password must have a lowercase letter!');
  }
}

void validateUppercase(String password) {
  final uppercase = RegExp(r'[A-Z]');
  if (!password.contains(uppercase)) {
    throw NoUppercaseException('Password must have an uppercase letter!');
  }
}

void validateNumber(String password) {
  final number = RegExp(r'[0-9]');
  if (!password.contains(number)) {
    throw NoNumberException('Password must have a number!');
  }
}
void validatePassword(String password) {
  validateLength(password);
  validateLowercase(password);
  validateUppercase(password);
  validateNumber(password);
}

Handling Custom Exceptions

Now that you have all the exceptions defined and the validation logic set up, you’re ready to use them. Replace the contents of main with the following code:

const password = 'password1234';

try {
  validatePassword(password);
  print('Password is valid');
} on ShortPasswordException catch (e) {
  print(e.message);
} on NoLowercaseException catch (e) {
  print(e.message);
} on NoUppercaseException catch (e) {
  print(e.message);
} on NoNumberException catch (e) {
  print(e.message);
}
Password must have an uppercase letter!

Challenges

Before moving on, here are some challenges to test your knowledge of error handling. It’s best if you try to solve them yourself, but solutions are available with the supplementary materials for this book if you get stuck.

Challenge 1: Double the Fun

Here’s a list of strings. Try to parse each of them with double.parse. Handle any format exceptions that occur.

final numbers = ['3', '1E+8', '1.25', 'four', '-0.01', 'NaN', 'Infinity'];

Challenge 2: Five Digits, No More, No Less

  • Create a custom exception named InvalidPostalCode.
  • Validate that a postal code is five digits.
  • If it isn’t, throw the exception.

Key Points

  • An error is something that crashes your app.
  • An exception is a known situation you must plan for and handle.
  • Not handling an exception is an error.
  • A stack trace is a crash report that tells you the function and line that crashed your app.
  • VS Code debugging tools allow you to set breakpoints and execute your code one line at a time.
  • try/catch blocks are one way to handle exceptions.
  • It’s better to handle specific exceptions with the on keyword rather than blindly handling all exceptions with catch.
  • If you have a logic error in your app, don’t “handle” it with catch. Let your app crash and then fix the bug.
  • Add a finally block to try-catch if you need to clean up resources.
  • You can create custom exceptions that implement Exception.

Where to Go From Here?

It’s a good thing when your app crashes while developing it. That’s a signal of something you need to fix. But when your app crashes for your users after you’ve published it, that’s not such a good thing. Some people might email you when they find a bug. Others might leave a negative review online, but most users won’t tell you about crashes or bugs. For that reason, you might consider using a third-party crash reporting library in your app. It’ll collect crash reports in a central location. Analyzing those reports will help you find and fix bugs you wouldn’t otherwise know about.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now