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.