Handle errors gracefully
Improve app robustness by handling errors. Learn about exceptions, errors, `try/catch`, `throw`, and `rethrow`.
In this chapter, make your application more robust
by handling errors gracefully.
Explore exceptions, try/catch blocks, and how to
create custom exceptions to manage errors in a structured way.
What you'll accomplish
Prerequisites
#Before you begin this chapter, ensure you:
-
Have completed Chapter 6 and have a
working Dart development environment with the
dartpediaproject. - Understand basic programming concepts like functions and classes.
Errors versus exceptions
#Dart distinguishes between two main types of failures: errors and exceptions.
| Concept | Exception |
Error |
|---|---|---|
| What it indicates | An expected runtime failure that code can recover from. | A programming bug or flaw in the code. |
| Common examples | FormatException, HttpException, SocketException. |
RangeError, TypeError, StateError. |
| Typical causes | Invalid user input, network failure, missing file. | Off-by-one index, calling a method on null. |
| How to handle | Catch and handle gracefully (for example, with try/catch). |
Do not catch; fix the bug in your code. |
Exceptions
#Exceptions represent conditions that you can anticipate and recover from. For example, when a user enters an unrecognized command-line argument, your application shouldn't crash with a raw stack trace. Instead, your code catches the exception, displays a friendly error message, and prompts the user with proper usage instructions.
In Dart, custom exceptions typically implement the Exception class or
extend an existing exception type like FormatException.
Errors
#
Errors represent bugs in your logic that
should be fixed during development rather than handled at runtime.
For example, accessing an element beyond the bounds of a list throws a
RangeError.
Attempting to catch and suppress a RangeError hides the bug and
can leave your application in an unpredictable state.
Instead, let errors propagate so you can inspect the stack trace and
fix the underlying mistake.
In Dart, classes that represent bugs extend or implement Error.
Tasks
#
The following tasks apply these principles to the command_runner package.
You define a custom exception class for invalid user arguments,
use try/catch blocks to intercept failures, and
configure CommandRunner to handle exceptions gracefully
while letting unexpected errors propagate.
Task 1: Create a custom ArgumentException
#
First, define a custom exception class called ArgumentException to
represent errors related to command-line arguments.
-
Create the file
command_runner/lib/src/exceptions.dart. This file contains the definition for yourArgumentExceptionclass. -
Define a class called
ArgumentExceptionthatextendsFormatException.command_runner/lib/src/exceptions.dartdartclass ArgumentException extends FormatException { /// The command that was parsed before discovering the error. /// /// This will be empty if the error was on the root parser. final String? command; /// The name of the argument that was being parsed when the error was /// discovered. final String? argumentName; ArgumentException( super.message, [ this.command, this.argumentName, super.source, super.offset, ]); @override String toString() { return 'ArgumentException: $message'; } }This class extends
FormatException, which is a built-in Dart class that implementsException. Invalid command-line arguments are an expected condition that callers can anticipate and handle gracefully, soArgumentExceptionis designed as an exception rather than anError. It includes additional properties to store the command and argument name associated with the error. This provides more context when handling the exception.command: The command that was being processed when the exception occurred.argumentName: The name of the argument that caused the exception.
Task 2: Implement error handling in CommandRunner
#
Next, update the CommandRunner class to
handle potential errors gracefully.
This involves adding an error-handling callback,
using try/catch to manage exceptions, and
throwing your new ArgumentException when the user provides bad input.
-
Add the necessary imports.
In
command_runner/lib/src/command_runner_base.dart, add imports fordart:async(to useFutureOr) and your newexceptions.dartfile.dartimport 'dart:async'; // Add this line import 'dart:collection'; import 'dart:io'; import 'arguments.dart'; import 'exceptions.dart'; // Add this line -
Add an
onErrorcallback to theCommandRunner.Modify the CommandRunner to accept an optional
onErrorfunction in its constructor. This allows users of your package to define their own error-handling logic.dartclass CommandRunner { // Add a constructor that accepts the optional callback. CommandRunner({this.onError}); final Map<String, Command> _commands = <String, Command>{}; UnmodifiableSetView<Command> get commands => UnmodifiableSetView<Command>(<Command>{..._commands.values}); // Define the onError property. FutureOr<void> Function(Object)? onError; // The rest of the class implementation... }This change introduces a nullable
onErrorproperty. TheFutureOr<void> Function(Object)?type means it's a function that takes anObjectand returns aFutureor nothing, and it might be null. -
Update the run method to use
try/catch.Wrap the logic inside the run method in a
try/catchblock. If an exception occurs, this block catches it and either passes it to theonErrorcallback or rethrows it if no callback is provided.rethrowpreserves the original error and stack trace.dartFuture<void> run(List<String> input) async { // [Step 6 update] try/catch added try { final ArgResults results = parse(input); if (results.command != null) { Object? output = await results.command!.run(results); print(output.toString()); } } on Exception catch (exception) { if (onError != null) { onError!(exception); } else { rethrow; } } }Notice the use of
on Exception catch (exception). In Dart, a barecatch (e)intercepts all thrown objects, includingErrorinstances. Specifyingon Exceptionensures that your code catches only recoverable exceptions, allowing bugs to propagate uncaught so you can fix them. -
Add validation to the
parsemethod.Finally, replace the existing
parsemethod incommand_runner_base.dartwith the following updated version. It includes checks that throw your customArgumentExceptionwhenever it detects invalid user input.dart// [Step 6 update] This method is replaced entirely. ArgResults parse(List<String> input) { ArgResults results = ArgResults(); if (input.isEmpty) return results; // Throw an exception if the command is not recognized. if (_commands.containsKey(input.first)) { results.command = _commands[input.first]; input = input.sublist(1); } else { throw ArgumentException( 'The first word of input must be a command.', null, input.first, ); } // Throw an exception if multiple commands are provided. if (results.command != null && input.isNotEmpty && _commands.containsKey(input.first)) { throw ArgumentException( 'Input can only contain one command. Got ${input.first} and ${results.command!.name}', null, input.first, ); } // Section: Handle options, including flags. Map<Option, Object?> inputOptions = {}; int i = 0; while (i < input.length) { if (input[i].startsWith('-')) { var base = _removeDash(input[i]); // Throw an exception if an option is not recognized for the given command. var option = results.command!.options.firstWhere( (option) => option.name == base || option.abbr == base, orElse: () { throw ArgumentException( 'Unknown option ${input[i]}', results.command!.name, input[i], ); }, ); if (option.type == OptionType.flag) { inputOptions[option] = true; i++; continue; } if (option.type == OptionType.option) { // Throw an exception if an option requires an argument but none is given. if (i + 1 >= input.length) { throw ArgumentException( 'Option ${option.name} requires an argument', results.command!.name, option.name, ); } if (input[i + 1].startsWith('-')) { throw ArgumentException( 'Option ${option.name} requires an argument, but got another option ${input[i + 1]}', results.command!.name, option.name, ); } var arg = input[i + 1]; inputOptions[option] = arg; i++; } } else { // Throw an exception if more than one positional argument is provided. if (results.commandArg != null && results.commandArg!.isNotEmpty) { throw ArgumentException( 'Commands can only have up to one argument.', results.command!.name, input[i], ); } results.commandArg = input[i]; } i++; } results.options = inputOptions; return results; } String _removeDash(String input) { if (input.startsWith('--')) { return input.substring(2); } if (input.startsWith('-')) { return input.substring(1); } return input; }This updated parse method now actively defends against bad input. Specifically, the new throw statements handle several common error cases:
- Unknown commands:
The first
if/elseblock ensures the first argument is a valid command. - Multiple commands: It checks that the user hasn't tried to run more than one command at a time.
- Unknown options:
The
orElseparameter withinfirstWherenow throws an exception if a user provides a flag or option (like--foo) that hasn't been defined for that command. - Missing option values:
It ensures that an option (like
--output) is followed by a value and not another option or the end of the input. - Too many arguments: It enforces a rule that commands can only have one positional argument.
- Unknown commands:
The first
Task 3: Update cli.dart to use the new error handling
#Modify cli/bin/cli.dart to use the new error handling in CommandRunner.
Open the
cli/bin/cli.dartfile.-
Update the
mainfunction to pass in anonErrorfunction to theCommandRunner:cli/bin/cli.dartdartimport 'package:command_runner/command_runner.dart'; const version = '0.0.1'; void main(List<String> arguments) { // [Step 6 update] Add onError method var commandRunner = CommandRunner( onError: (Object error) { if (error is Error) { throw error; } if (error is Exception) { print(error); } }, )..addCommand(HelpCommand()); commandRunner.run(arguments); }This code passes an
onErrorcallback toCommandRunner. The callback rethrows anyErrorso the application crashes, while printing anyExceptionto the console so the user sees the error message.
Task 4: Update command_runner library exports
#Make ArgumentException available to the command_runner library.
-
Open
command_runner/lib/command_runner.dart, and add the following exports:command_runner/lib/command_runner.dartdart/// Support for doing something awesome. /// /// More dartdocs go here. library; export 'src/arguments.dart'; export 'src/command_runner_base.dart'; export 'src/help_command.dart'; export 'src/exceptions.dart'; // Add this line // TODO: Export any libraries intended for clients of this package.In Dart packages, files inside
lib/src/are private implementation details. Exportingsrc/exceptions.dartfromlib/command_runner.dartexposesArgumentExceptionas part of the package's public API so callers (likecli.dart) can import and use it.
Task 5: Test the new error handling
#Test the new error handling by running the application with invalid arguments.
Open your terminal and navigate to the
clidirectory.-
Run the command
dart run bin/cli.dart invalid_command.You should see the following output:
bashArgumentException: The first word of input must be a command.This confirms that the
ArgumentExceptionis being thrown and caught correctly.
Review
#What you accomplished
Here's a summary of what you built and learned in this lesson.Distinguished errors from exceptions
You learned that subtypes of Error indicate programming bugs and aren't intended to be caught, while subtypes of
Exception represent recoverable failures that your code can handle gracefully.
Used try-catch blocks to handle failures
You wrapped risky code in try-catch blocks to intercept exceptions. You used
on ExceptionType catch (e) to handle specific types and rethrow to repropagate exceptions while preserving the stack trace.
Created and threw custom exceptions
You built a ArgumentException class that extends FormatException to provide context-rich error information. Then you used
throw to signal validation failures with meaningful messages for better debugging and user feedback.
Quiz
#Check your understanding
1 / 4-
Throw an exception describing what went wrong.
That's right!
Throwing an exception immediately stops the invalid code path, provides a clear error message, and forces the caller to handle the error explicitly.
-
Return
nulland let the caller check for it.Not quite.
Returning
nullforces callers to remember to check for it. If they forget, bugs can occur silently. There's a more explicit way to signal problems. -
Print an error message and continue execution.
Not quite.
Printing doesn't stop invalid data from being used. The function would still need to return something, potentially causing bugs downstream.
-
Set a global error flag that other code can check.
Not quite.
Global state is error-prone and easy to forget. This approach doesn't force callers to handle the error.
throw and rethrow in a catch block?
-
throwalways creates a new stack trace;rethrowpreserves the original stack trace.That's right!
Use
rethrowwhen you want to log or partially handle an exception but still let it propagate with its original stack trace intact for debugging. -
throwcreates a new exception;rethrowjust prints the current one.Not quite.
Neither one prints anything.
rethrowpropagates the exception, it doesn't just display it. -
throwworks inside catch blocks, whilerethrowonly works outside them.Not quite.
It's closer to the opposite.
rethrowonly works inside catch blocks.throwworks anywhere. -
throwis for errors;rethrowis for exceptions.Not quite.
Both work with any throwable object. The difference isn't about the type of object being thrown.
try { riskyOperation(); } on FormatException catch (e) { print(e); }. What happens if riskyOperation() throws an HttpException?
-
The
HttpExceptionpropagates up, uncaught by this try/catch.That's right!
The
onclause filters by exception type. SinceHttpExceptionisn't aFormatException, it bypasses this catch block entirely. -
The
HttpExceptionis caught and printed.Not quite.
The
on FormatExceptionclause only catchesFormatExceptionand its subtypes.HttpExceptionis a different type. -
The program crashes because you can't use
onwithcatch.Not quite.
on Type catch (e)is valid syntax that filters which exceptions to catch. The issue is the type mismatch, not the syntax. -
The exception is silently ignored and execution continues.
Not quite.
Exceptions are never silently ignored in Dart. If a catch block doesn't match, the exception doesn't just disappear.
Error and an Exception?
-
An
Errorindicates a programming bug that shouldn't be caught, while anExceptionrepresents an expected condition that code can recover from.That's right!
Subtypes of
Error(such asRangeErrororTypeError) indicate bugs in code that you should fix. Subtypes ofException(such asFormatExceptionorHttpException) represent conditions that your code can anticipate and handle gracefully. -
Erroris thrown by the Dart runtime, whileExceptioncan only be thrown by user code.Not quite.
Both the Dart runtime and user code can throw exceptions and errors.
-
Exceptionrequires atry/catchblock, butErroris ignored if uncaught.Not quite.
Uncaught errors and uncaught exceptions both terminate execution. Neither is silently ignored.
-
ErrorandExceptionare identical in Dart and can be used interchangeably.Not quite.
Dart distinguishes between them by design: errors indicate bugs, while exceptions represent recoverable failures.
Next lesson
#In the next lesson, learn about advanced object-oriented features in Dart, including enhanced enums and extensions. Improve the output formatting and add color to your CLI application.
Unless stated otherwise, the documentation on this site reflects Dart 3.13.3. Page last updated on 2026-09-15. View source or report an issue.