Mastering Dart
上QQ阅读APP看书,第一时间看更新

Errors versus exceptions

Runtime faults can and do occur during the execution of a Dart program. We can split all faults into two types:

  • Errors
  • Exceptions

There is always some confusion on deciding when to use each kind of fault, but you will be given several general rules to make your life a bit easier. All your decisions will be based on the simple principle of recoverability. If your code generates a fault that can reasonably be recovered from, use exceptions. Conversely, if the code generates a fault that cannot be recovered from, or where continuing the execution would do more harm, use errors.

Let's take a look at each of them in detail.

Errors

An error occurs if your code has programming errors that should be fixed by the programmer. Let's take a look at the following main function:

main() {
  // Fixed length list
  List list = new List(5);
  // Fill list with values
  for (int i = 0; i < 10; i++) {
    list[i] = i;
  }
  print('Result is ${list}');
}

We created an instance of the List class with a fixed length and then tried to fill it with values in a loop with more items than the fixed size of the List class. Executing the preceding code generates RangeError, as shown in the following screenshot:

This error occurred because we performed a precondition violation in our code when we tried to insert a value in the list at an index outside the valid range. Mostly, these types of failures occur when the contract between the code and the calling API is broken. In our case, RangeError indicates that the precondition was violated. There are a whole bunch of errors in the Dart SDK such as CastError, RangeError, NoSuchMethodError, UnsupportedError, OutOfMemoryError, and StackOverflowError. Also, there are many others that you will find in the errors.dart file as a part of the dart.core library. All error classes inherit from the Error class and can return stack trace information to help find the bug quickly. In the preceding example, the error happened in line 6 of the main method in the range_error.dart file.

We can catch errors in our code, but because the code was badly implemented, we should rather fix it. Errors are not designed to be caught, but we can throw them if a critical situation occurs. A Dart program should usually terminate when an error occurs.

Exceptions

Exceptions, unlike errors, are meant to be caught and usually carry information about the failure, but they don't include the stack trace information. Exceptions happen in recoverable situations and don't stop the execution of a program. You can throw any non-null object as an exception, but it is better to create a new exception class that implements the abstract class Exception and overrides the toString method of the Object class in order to deliver additional information. An exception should be handled in a catch clause or made to propagate outwards. The following is an example of code without the use of exceptions:

import 'dart:io';

main() {
  // File URI
  Uri uri = new Uri.file("test.json");
  // Check uri
  if (uri != null) {
    // Create the file
    File file = new File.fromUri(uri);
    // Check whether file exists
    if (file.existsSync()) {
      // Open file
      RandomAccessFile random = file.openSync();
      // Check random
      if (random != null) {
        // Read file
        List<int> notReadyContent = 
          random.readSync(random.lengthSync());
        // Check not ready content
        if (notReadyContent != null) {
          // Convert to String
          String content = new 
            String.fromCharCodes(notReadyContent);
          // Print results
          print('File content: ${content}');
        }
        // Close file
        random.closeSync();
      }
    } else {
      print ("File doesn't exist");
    }
  }
}

Here is the result of this code execution:

File content: [{ name: Test, length: 100 }]

As you can see, the error detection and handling leads to a confusing spaghetti code. Worse yet, the logical flow of the code has been lost, making it difficult to read and understand it. So, we transform our code to use exceptions as follows:

import 'dart:io';

main() {
  RandomAccessFile random;
  try {
    // File URI
    Uri uri = new Uri.file("test.json");
    // Create the file
    File file = new File.fromUri(uri);
    // Open file
    random = file.openSync();
    // Read file
    List<int> notReadyContent = 
      random.readSync(random.lengthSync());
    // Convert to String
    String content = new String.fromCharCodes(notReadyContent);
    // Print results
    print('File content: ${content}');
  } on ArgumentError catch(ex) {
    print('Argument error exception');
  } on UnsupportedError catch(ex) {
    print('URI cannot reference a file');
  } on FileSystemException catch(ex) {
    print ("File doesn't exist or accessible");
  } finally {
    try {
      random.closeSync();
    } on FileSystemException catch(ex) {
      print("File can't be close");
    }
  }
}

The code in the finally statement will always be executed independent of whether the exception happened or not to close the random file. Finally, we have a clear separation of exception handling from the working code and we can now propagate uncaught exceptions outwards in the call stack.

The suggestions based on recoverability after exceptions are fragile. In our example, we caught ArgumentError and UnsupportError in common with FileSystemException. This was only done to show that errors and exceptions have the same nature and can be caught any time. So, what is the truth? While developing my own framework, I used the following principle:

If I believe the code cannot recover, I use an error, and if I think it can recover, I use an exception.

Let's discuss another advanced technique that has become very popular and that helps you change the behavior of the code without making any changes to it.