anyhow
Anyhow offers versatile and idiomatic error handling capabilities to make your code safer, more maintainable, and errors easier to debug.
This is accomplished through providing a Dart implementation of the Rust Result monad type, and
an implementation of the popular Rust crate with the same name - anyhow.
Anyhow will allow you to never throw another exception again and have a predictable control flow. When
errors do arise, you can add context
to better understand the situation that led to the errors.
See Anyhow Result Type Error Handling to jump right into an example.
Table of Contents
- What Is a Result Monad Type And Why Use it?
- Intro to Usage
- Configuration Options
- Adding Predictable Control Flow To Legacy Dart Code
- Dart Equivalent To The Rust "?" Operator
- How to Never Unwrap Incorrectly
- Misc
What Is a Result Monad Type And Why Use it?
A monad is just a wrapper around an object that provides a standard way of interacting with the inner object. The
Result
monad is used in place of throwing exceptions. Instead of a function throwing an exception, the function
returns a Result
, which can either be a Ok
(Success) or Err
(Error/Failure), Result
is the type union
between the two. Before unwrapping the inner object, you check the type of the Result
through conventions like
case Ok(:final ok)
and isOk()
. Checking allows you to
either resolve any potential issues in the calling function or pass the error up the chain until a function resolves
the issue. This provides predictable control flow to your program, eliminating many potential bugs and countless
hours of debugging.
Intro to Usage
Regular Dart Error handling
void main() {
try {
print(order("Bob", 1));
} catch(e) {
print(e);
}
}
String order(String user, int orderNumber) {
final result = makeFood(orderNumber);
return "Order of $result is complete for $user";
}
String makeFood(int orderNumber) {
if (orderNumber == 1) {
return makeHamburger();
}
return "pasta";
}
String makeHamburger() {
// Who catches this??
// How do we know we won't forget to catch this??
// What is the context around this error??
throw "Hmm something went wrong making the hamburger.";
}
Output
Hmm something went wrong making the hamburger.
What's Wrong with Solution?
- If we forget to catch in the correct spot, we just introduced a bug or worse - crashed our entire program.
- We may later reuse
makeHamburger
,makeFood
, ororder
, and forget that it can throw. - The more we reuse functions that can throw, the less maintainable and error-prone our program becomes.
- Throwing is also an expensive operation, as it requires stack unwinding.
The Better Ways To Handle Errors With Anyhow
Other languages address the throwing exception issue by preventing them entirely. Most that do use a Result
monad.
Base Result Type Error Handling
With the base Result
type, implemented based on the Rust standard Result type, there are no more undefined
behaviours due to control flow.
import 'package:anyhow/base.dart';
void main() {
print(order("Bob", 1));
}
Result<String, String> order(String user, int orderNumber) {
final result = makeFood(orderNumber);
if(result case Ok(:final ok)) { // Could also use "if(result.isOk())" or a switch statement
return Ok("Order of $ok is complete for $user");
}
return result;
}
Result<String, String> makeFood(int orderNumber) {
if (orderNumber == 1) {
return makeHamburger();
}
return Ok("pasta");
}
Result<String,String> makeHamburger() {
// What is the context around this error??
return Err("Hmm something went wrong making the hamburger.");
}
Output
Hmm something went wrong making the hamburger.
Anyhow Result Type Error Handling
With the Anyhow Result
type, we can now add any Object
as context around errors. To do so, we can use context
or
withContext
(lazily). Either will only have an effect if a Result
is the Err
subclass. In the following
example we will use String
s as the context, but using Exception
s, especially for the root cause is common practice
as well.
import 'package:anyhow/anyhow.dart';
void main() {
print(order("Bob", 1));
}
Result<String> order(String user, int orderNumber) {
final result = makeFood(orderNumber).context("Could not order for user: $user.");
if(result case Ok(:final ok)) {
return Ok("Order of $ok is complete for $user");
}
return result;
}
Result<String> makeFood(int orderNumber) {
if (orderNumber == 1) {
return makeHamburger().context("Order number $orderNumber failed.");
}
return Ok("pasta");
}
Result<String> makeHamburger() {
return bail("Hmm something went wrong making the hamburger.");
}
Output
Error: Could not order for user: Bob.
Caused by:
0: Order number 1 failed.
1: Hmm something went wrong making the hamburger.
StackTrace:
#0 AnyhowResultExtensions.context (package:anyhow/src/anyhow/anyhow_extensions.dart:12:29)
#1 order (package:anyhow/test/src/temp.dart:9:40)
#2 main (package:anyhow/example/main.dart:5:9)
... <OMITTED FOR EXAMPLE>
What Would This Look Like Without Anyhow
Before Anyhow, if we wanted to accomplish something similar, we had to do:
void main() {
print(order("Bob", 1));
}
Result<String, String> order(String user, int orderNumber) {
final result = makeFood(orderNumber);
if(result case Ok(:final ok)) {
return Ok("Order of $ok is complete for $user");
}
Logging.w("Could not order for user: $user.");
return result;
}
Result<String, String> makeFood(int orderNumber) {
if (orderNumber == 1) {
final result = makeHamburger();
if (result.isErr()) {
Logging.w("Order number $orderNumber failed.");
}
return result;
}
return Ok("pasta");
}
Result<String, String> makeHamburger() {
// What is the context around this error??
return Err("Hmm something went wrong making the hamburger.");
}
Which is more verbose/error-prone and may not be what we actually want. Since:
- We may not want to log anything if the error state is known and can be recovered from
- Related logs should be kept together (in the example, other functions could log before this Result had been handled)
- We have no way to get the correct stack traces related to the original issue
- We have no way to inspect "context", while with anyhow we can iterate through with
chain()
Now with anyhow, we are able to better understand and handle errors in an idiomatic way.
Base Result Type vs Anyhow Result Type
The base Result
Type and the anyhow Result
Type can be imported with
import 'package:anyhow/base.dart' as base;
or
import 'package:anyhow/anyhow.dart' as anyhow;
Respectively. Like in anyhow, these types have parity, thus can be used together
import 'package:anyhow/anyhow.dart' as anyhow;
import 'package:anyhow/base.dart' as base;
void main(){
base.Result<int,anyhow.Error> x = anyhow.Ok(1); // valid
anyhow.Result<int> y = base.Ok(1); // valid
anyhow.Ok(1).context(1); // valid
base.Ok(1).context(1); // not valid
}
The base Result
type is the standard implementation of the Result
type and the anyhow Result
type is a typedef
typedef Result<S> = base.Result<S, anyhow.Error>
with the power of anyhow.Error
and additional extensions. Most of the time you should just use the anyhow Result type.
If you don't want to import both libraries like above, and you need use both in the same file, you can just import the
anyhow one and use the Base
prefix where necessary.
import 'package:anyhow/anyhow.dart';
void main(){
BaseResult<int,String> x = BaseErr("this is an error message");
BaseResult<int, Error> y = x.mapErr(anyhow); // or just toAnyhowResult()
Result<int> w = y; // just for explicitness in the example
assert(w.unwrapErr().downcast<String>().unwrap() == "this is an error message");
}
Configuration Options
Anyhow functionality can be changed by changing:
Error.hasStackTrace;
Error.displayFormat;
Error.stackTraceDisplayFormat;
Error.stackTraceDisplayModifier;
Which is usually done at startup.
hasStackTrace
: WithError.hasStackTrace = false
, we can exclude capturing a stack trace:
Error: Could not order for user: Bob.
Caused by:
0: Order number 1 failed.
1: Hmm something went wrong making the hamburger.
displayFormat
: We can view the root cause first withError.displayFormat = ErrDisplayFormat.rootCauseFirst
Root Cause: Hmm something went wrong making the hamburger.
Additional Context:
0: Order number 1 failed.
1: Could not order for user: Bob.
StackTrace:
#0 bail (package:anyhow/src/anyhow/functions.dart:6:14)
#1 makeHamburger (package:anyhow/test/src/temp.dart:31:10)
... <OMITTED FOR EXAMPLE>
-
stackTraceDisplayFormat
: if we want to includenone
, themain
, orall
stacktraces in the output. -
stackTraceDisplayModifier
: Modifies the stacktrace during display. Useful for adjusting number of frames to include during display/logging.
Adding Predictable Control Flow To Legacy Dart Code
At times, you may need to integrate with legacy code that may throw or code outside your project. To handle, you
can just wrap in a helper function like executeProtected
void main() {
Result<int> result = executeProtected(() => functionWillThrow());
print(result);
}
int functionWillThrow() {
throw "this message was thrown";
}
Output:
Error: this message was thrown
Dart Equivalent To The Rust "?" Operator
In Dart, the Rust "?" operator functionality in x?
, where x
is a Result
, can be accomplished with
if (x case Err()) {
return x.into();
}
into
may be needed to change the S
type of Result<S,F>
for x
to that of the functions return type if
they are different.
into
only exits if after the type check, so you will never mishandle a type change since the compiler will stop you.
Note: There also exists
intoUnchecked
that does not require implicit cast of a Result
Type.
How to Never Unwrap Incorrectly
In Rust, as here, it is possible to unwrap values that should not be unwrapped:
if (x.isErr()) {
return x.unwrap(); // this will panic (should be "unwrapErr()")
}
To never unwrap incorrectly, simple do a typecheck with is
or case
instead of isErr()
.
if (x case Err(:final err)){
return err;
}
and vice versa
if (x case Ok(:final ok){
return ok;
}
The type check does an implicit cast, and we now have access to the immutable error and ok value respectively.
Similarly, we can mimic Rust's match
keyword, with Dart's switch
switch(x){
case Ok(:final ok):
print(ok);
case Err(:final err):
print(err);
}
final y = switch(x){
Ok(:final ok) => ok,
Err(:final err) => err,
};
Or declaratively with match
x.match(
ok: (ok) => ok,
err: (err) => err
);
Or even with mapOrElse
Misc
Working with Futures
When working with Future
s it is easy to make a mistake like this
Future.delayed(Duration(seconds: 1)); // Future not awaited
Where the future is not awaited. With Result's (Or any wrapped type) it is possible to make this mistake
await Ok(1).map((n) async => await Future.delayed(Duration(seconds: n))); // Outer "await" has no effect
The outer "await" has no effect since the value's type is Result<Future<void>>
not Future<Result<void>>
.
To address this use toFutureResult()
await Ok(1).map((n) async => await Future.delayed(Duration(seconds: n))).toFutureResult(); // Works as expected
To avoid these issues all together in regular Dart and with wrapped types like Result
, it is recommended to enable
these Future
linting rules in analysis_options.yaml
linter:
rules:
unawaited_futures: true # Future results in async function bodies must be awaited or marked unawaited using dart:async
await_only_futures: true # "await" should only be used on Futures
avoid_void_async: true # Avoid async functions that return void. (they should return Future<void>)
#discarded_futures: true # Don’t invoke asynchronous functions in non-async blocks.
analyzer:
errors:
unawaited_futures: error
await_only_futures: error
avoid_void_async: error
#discarded_futures: error
Working With Iterable Results
In addition to useful .toErr()
, .toOk()
extension methods, anyhow provides a .toResult()
on types that can be
converted to a single result. One of these is on Iterable<Result<S,F>>
, which can turn into a
Result<List<S>,List<F>>
. Also, there is .toResultEager()
which can turn into a single Result<List<S>,F>
.
var result = [Ok(1), Ok(2), Ok(3)].toResult();
expect(result.unwrap(), [1, 2, 3]);
result = [Ok<int,int>(1), Err<int,int>(2), Ok<int,int>(3)].toResultEager();
expect(result.unwrapErr(), 2);
Panic
Rust vs Dart Error handling terminology:
Dart Exception Type | Equivalent in Rust |
---|---|
Exception | Error |
Error | Panic |
Thus, here Error
implements Dart core Exception
(Not to be confused with the
Dart core Error
type)
import 'package:anyhow/anyhow.dart' as anyhow;
import 'package:anyhow/base.dart' as base;
base.Result<String,anyhow.Error> x = anyhow.Err(1); // == base.Err(anyhow.Error(1));
And Panic
implements Dart core Error
.
if (x.isErr()) {
return x.unwrap(); // this will throw a Panic (should be "unwrapErr()")
}
As with Dart core Error
s, Panic
s should never be caught.
Anyhow was designed with safety in mind. The only time anyhow will ever throw is if you unwrap
incorrectly (as above),
in
this case it will throw a Panic
. See How to Never Unwrap Incorrectly section to
avoid ever using unwrap
.
Null and Unit
In Dart, void
is used to indicate that a function doesn't return anything or a type should not be used, as such:
Result<void, void> x = Ok(1); // valid
Result<void, void> y = Err(1); // valid
int z = x.unwrap(); // not valid
Since stricter types are preferred and Err
cannot be null:
Result<void, void> x = Ok(null); // valid
Result<void, void> x = Err(null); // not valid
Therefore use ()
or Unit
:
Unit == ().runtimeType; // true
Result<(), ()> x = Err(unit); // valid
Result<Unit, Unit> y = Err(()); // valid
x == y; // true
// Note:
// const unit = const ();
// const okay = const Ok(unit);
// const error = const Err(unit);
Infallible
Infallible
is the error type for errors that can never happen. This can be useful for generic APIs that use Result
and parameterize the error type, to indicate that the result is always Ok.Thus these types expose intoOk
and
intoErr
.
Result<int, Infallible> x = Ok(1);
expect(x.intoOk(), 1);
Result<Infallible, int> w = Err(1);
expect(w.intoErr(), 1);
typedef Infallible = Never;
See examples for more.