language
language copied to clipboard
First-class, functional-style `Result<E, T>` types with built-in operators
Proposal
With sealed classes and exhaustive pattern matching, I think we've hit a natural time to add first-class result types to the language, or at least the first-party libraries (the ones published by dart.dev
):
sealed class Result<E, T> {
R match<R>({
required R Function(E) onError,
required R Function(T) onValue,
}) => switch (this) {
Failure(error: final error) => onError(error),
Success(value: final value) => onValue(value),
};
T onError(T Function(E) onError) => switch (this) {
Failure(error: final error) => onError(error),
Success(value: final value) => value,
};
T assertErrors() => switch (this) {
Failure(error: final error) => throw StateError("Result type had an error value: $error"),
Success(value: final value) => value,
};
}
class Failure<E, T> extends Result<E, T> {
final E error;
Failure(this.error);
}
class Success<E, T> extends Result<E, T> {
final T value;
Success(this.value);
}
Usage would look like:
Result<String, int> getNum() =>
Random().nextBool() ? Success(123) : Failure("Oops!");
void main() {
// Simply handle each case
getNum().match(
onError: (reason) => print("Couldn't get number because $reason"),
onValue: (value) => print("Got number! ${value + 1}"),
);
// Use a value based on the presence of an error
final number = getNum().match(
onError: (error) => 0,
onValue: (value) => value + 1,
);
// or if you just want to handle errors (like ?? in null safety)
final num2 = getNum().onError((error) => 0);
// if you believe there are no errors (like ! in null safety)
final num3 = getNum().assertErrors();
}
Possible lints
Seeing as you could forget to handle an error, this should probably come with a lint against cases like the following:
void main() {
getNum(); // Warning: Avoid using Result types without handling the error case
}
Another lint could be
Warning: Avoid returning nullable Result types. Use a nullable T
type instead
BAD:
Result<ErrorType, User>? getUser() => isPresent(user) ? Success(user) : null;
GOOD:
Result<ErrorType, User?> getUser() => Success(isPresent(user) ? user : null);
Motivation
Besides the fact that this style is a pretty popular alternative to try/catch-ing errors that functions may throw, this solves the concrete problem of "how can I be sure this function won't crash my app?". By returning a Result
type, you're signaling to the caller that all error cases can be handled safely by using pattern matching or a method like .onError
. With more and more of the ecosystem using a style like this, code will be safer at runtime. As mentioned above, sealed classes and exhaustive pattern matching allow us to handle this safely for the first time, so this would be a natural time to officially adopt the Result
pattern.
In fact, code in the wild already does use this style:
-
package:fpdart
'sEither
class -
package:either
'sEither
class -
package:async
'sResult
class - ...and the countless other packages seen by searching
either
,result
, oroption
As much as this "could be handled by a package", right now the ecosystem is fragmented because it is being handled by packages, and each package is making their own, meaning the chances of any two packages being compatible are slim to none. Sure, we could make a "better" package, but that probably wouldn't help,. By making this first-party, it is more likely that other packages will contribute and migrate to the canonical implementation, increasing compatibility across the ecosystem.
Another thing is that without being a language feature or recognized first-party package, we miss out on valuable lints.
Possible language/syntax improvements
If this is made a first-class language feature, not just package, we can go a little further. Above, I noted that Result.onError
was like ??
in the sense that it says "if there is an error/null, ignore it and use this value instead". Result.assertErrors
is like !
in that it says "I don't expect there to be an error/null, so if there is, please throw an actual error and stop execution".
I believe those concepts are similar enough to null safety that they shouldn't be considered semantic overloading, while also being different enough to not be confused with actual null safety (the above lints would protect against Result?
messiness).
To further demonstrate, compare these two getUser
functions, which return a Result<String, User>
and a User?
:
class User {
String get name => "John Smith";
}
Result<String, User> getUser1() =>
Random().nextBool() ? Success(User()) : Failure("Could not get user");
User? getUser2() => Random().nextBool() ? User() : null;
void main() {
final name1 = getUser1().assertErrors().name;
final user1 = getUser1().onError((_) => User());
final name2 = getUser2()!.name;
final user2 = getUser2() ?? "Default name";
}
By thinking a little harder, we can get an analog to the ?.
operator:
sealed class Result<E, T> {
Result<E, R> chain<R>(R Function(T) func) => switch (this) {
Failure(error: final error) => Failure(error),
Success(value: final value) => Success(func(value)),
};
}
// Same getUser1 and getUser2 as above
void main() {
final name3 = getUser1().chain((user) => user.name);
final name4 = getUser2()?.name;
}
Thoughts?
It's similar to nullability because both are union types, and both have a second part which is mostly untyped. (You could type the error, but most of the time you just want to know that it's not the value. And you don't really want the supertype to have to carry an error type too.)
Which is a different way of saying that they're both monads. The null monad and the error monad are both classical monads.
The difference is that null
has always been a value in Dart, not part of control flow. We don't have a "no value" type, like fx C's void
type, which null
would be a reflection of.
And errors have always, mostly, been control flow, even when we capture them in async, they're intended to be reified as control flow and caught by catch
.
So this is, effectively, asking for an official ErrorOr
union type, and monadic operations on it, probably similar to those of both nullable types and FutureOr
.
-
e?.useValue
preserves the error, or operates on the value. -
e ?? e2
uses the value ofe
, or ignores the error and uses the value ofe2
. -
awaitish e
evaluates to the value or re-raises the error (reifies the capture control flow).
Not sure we really do need language features for most of these. The ?.
is the one that I haven't found a good way to do (in the time writing this).
The rest are easy to get to without to much extra writing.
(Also, package:async
is published by dart.dev
.)
An official implementation should also ensure that it integrates nicely with the existing language:
- The error variant should carry stack traces -- maybe allow the Error variant to only contain types that inherit from Dart exceptions?
- There should be a function that maps a throwing function to one that returns a Result
- There should be a function that maps a result to a nullable
- Unwrapping the Error variant should make in the StackTrace / error clear, where the original Error happened and where the unwrap took place
- Documentation should clarify where Result / Either makes sense and where exceptions make more sense (stuff like division by zero for example)
In Rust a problem with Results is that your custom error types often need an Any variant for errors you just want to bubble up. Fortunately Dart already has a solution for that: throwing Exceptions.
It's similar to nullability because both are union types, and both have a second part which is mostly untyped. (You could type the error, but most of the time you just want to know that it's not the value. And you don't really want the supertype to have to carry an error type too.)
I would use Results exactly when the Error type is not untyped, but either an enum or a sealed class that allows me to handle the errors. If I don't care about the error type, in Dart I'd either use nullables directly or throw exceptions.
And errors have always, mostly, been control flow, even when we capture them in async, they're intended to be reified as control flow and caught by
catch
.
Not sure what you mean, but I'd argue that Results are much better for control flow as they work with the new pattern matching features, traditional if statements and could also be integrated with the collection APIs
In general: Exceptions work pretty badly with collections and iterators (will the iterator continue running after an exception was thrown or will it throw itself? will it return the first exception or all exceptions)) -- A Result type would be great here!
So this is, effectively, asking for an official
ErrorOr
union type, and monadic operations on it, probably similar to those of both nullable types andFutureOr
.
e?.useValue
preserves the error, or operates on the value.e ?? e2
uses the value ofe
, or ignores the error and uses the value ofe2
.awaitish e
evaluates to the value or re-raises the error (reifies the capture control flow).
No, to the contrary, FutureOr shouldn't exist
(Also,
package:async
is published bydart.dev
.)
The language features, like the ? and ! operators would be pretty important and the lint when ignoring error cases would be absolutely critical. If it was added as a package it should be included in the sdk by default.
@lrhn
It's similar to nullability because both are union types, and both have a second part which is mostly untyped. (You could type the error, but most of the time you just want to know that it's not the value. And you don't really want the supertype to have to carry an error type too.)
Pretty much, yeah. In cases where you do need to know the error type, you couldn't use the ??
operator and its friends -- you'd have to use .match()
And errors have always, mostly, been control flow, even when we capture them in async, they're intended to be reified as control flow and caught by
catch
.
I see a difference here. Error
s are meant to be thrown and stop the flow of the program. Those are control flow entities. But Exception
s were always meant to be handled. I'd argue the fact that you throw them isn't as important as the fact that you hope someone else will eventually catch it. In an ideal user-facing application, all or most Exception
s would be handled.
I see Result types as corresponding to Exception
s, and therefore a value, while I still see throw
as a tool to surface Error
s, which therefore affects control flow.
So this is, effectively, asking for an official
ErrorOr
union type, and monadic operations on it, probably similar to those of both nullable types andFutureOr
.
e?.useValue
preserves the error, or operates on the value.e ?? e2
uses the value ofe
, or ignores the error and uses the value ofe2
.awaitish e
evaluates to the value or re-raises the error (reifies the capture control flow).
Yes to the first two but probably not the last one (unless you mean e!
), because again I don't see exceptions as needing to stop control flow, I see them as signals that need to be interpreted, like how nulls once were. The only reason I'd suggest e!
is because someone will want to eg, make an HTTP request and just crash the program if the status code isn't a clean 200. But in the ideal case, e!
should never be used, and the exception should instead be handled (eg, a print statement or a Flutter Text
).
I guess in that way, i see Exceptions as similar to nullables for another reason: we used to have lots of null checks everywhere and throw an error if we found once, but post null-safety we can simply ?.
and ??
it away. What once greatly affected control flow is now just an expression. I believe if Result types catch on, the same would be true for Exceptions. Yes, IndexError
s should probably still break your program, but now server errors won't.
Not sure we really do need language features for most of these. The
?.
is the one that I haven't found a good way to do (in the time writing this).
The operators would be nice to have, and as I demonstrated they can all be written as methods today including ?.
, but IMO the standardization is way more important. Yes, package:async
is first-party but its Result
class is not advertised as a true Result class (and even if it were, I'd be happy to pretty much just move it somewhere more broad than async
.
@benthillerkus
- The error variant should carry stack traces -- maybe allow the Error variant to only contain types that inherit from Dart exceptions?
I'd disagree again and say that Result types should almost never translate to thrown errors in the ideal case (barring a quick-and-dirty e!
syntax), but the whole point of them is to always be guaranteed to be handled safely in-app, in a way that makes sense for your specific use-case.
I agree with throwing --> Result, but I don't see the need for Result --> nullable (especially if you could just do e ?? null
).
I see a difference here.
Error
s are meant to be thrown and stop the flow of the program. Those are control flow entities. ButException
s were always meant to be handled
That's a convention, the language doesn't care. The only special casing is for objects which extend the Error
class.
After throwing, they're all just objects.
A language feature should be able to handle any object that is thrown, so anything other than null
.
I think that with extension types, we'll see new implementations of the existing Result
(and Option
) types, and probably other stable helper types.
Consider something like this (slightly hackish) version; https://dartpad.dev/?id=4daa370e9f33c8562ca3efe06e8e5adf&channel=master
Not sure I see the practical difference between extension types over sealed types in terms of actual handling (other than that devs can make their extension types off of Result
I guess), but yes the point is that with exhaustive pattern matching we've reached a point where these classes are possible idiomatically.
And it's precisely because packages can and have been implementing their own versions that I feel it should be standardized as early as possible. Because with the current fragmentation, the result types from two packages are very unlikely to be compatible.
Also just a note that Optional<T>
types isn't what I'm looking for here as I believe null safety is enough in these cases. It's Result<E, T>
that handles a possible error value. And to address
A language feature should be able to handle any object that is thrown, so anything other than
null
.
I don't think the E
type necessarily needs to or should be thrown. I can imagine
Result<String?, User> getUser(); // returns the reason this failed, or null if some unknown error occurred
void main() => getUser().match(
onError: (reason) => print(reason ?? "Something happened"),
onValue: (user) => print(user.value),
);
The point of my distinction between Error
s and Exception
s wasn't to constrain it to the E extends Exception
, but more philosophically about how Result
types turn a control-based throw/try/catch into an expression-based .match
,/.onError
(or even simpler, ??
/?.
/!
). That's also why I didn't get what you meant by awaitish
earlier -- I'm not saying "defer this exception but maybe throw it", but rather "this Result
subclass will definitely be a Success
type, but if it's a Failure
then throw".
Not sure I see the practical difference between extension types over sealed types in terms of actual handling (other than that devs can make their extension types off of
Result
I guess), but yes the point is that with exhaustive pattern matching we've reached a point where these classes are possible idiomatically.
The difference is it is free (wrapperless), much more similar to FutureOr
today.
Here is your example converted roughly https://dartpad.dev/?id=12c5ce9064357d6bb370cb2027612aa8&channel=master
I've been writing more and more Rust lately due to Advent of Code and it really makes me appreciate a strong Result
type and I definitely would love to see an official version with language support in Dart. This would require changing official APIs like num.parse
, jsonDecode
/Encode
, etc. A migration period might look like num.parseResult()
or num.parseWithResult()
, etc but that's probably for another issue.
With full language support, there is no need to change APIs.
Let's say we introduce a postfix operator, strawman syntax catch
, which converts an expression of type T
to an expression of type Result<T>
, and captures either a value or a throw.
And we allow !
to convert Result<T>
to T
like suggested.
Then we don't need to change int.parse
, you just write var r = int.parse(string) catch;
.
(Hmm, might look better as prefix, like catch(e)
.)
(Hmm, might look better as prefix, like catch(e).)
I'm more fond of a prefix like try
. So for example, you'd do var r = try int.parse(string)
.
I think the goal of having a core Result
class should be to remove the ambiguity of determining whether or not a method can "throw" an exception. People would just shove try
(or whatever the syntax becomes) for every method, even when unnecessary. I think that'd be fine for a migration period while the APIs are updated (then a lint would come along like unnecessary_try
, but if there would be no plans to do so, then I fail to see the point.
I don't see us changing existing APIs to return a Result<T>
rather than T
+throwing.
And I don't see us writing new APIs that way either, to be honest.
Mainly because I don't actually expect us to add a language supported Result
type.
It would be a massive change of direction to do that. Not necessarily a bad direction, if one had started going that way from the start, but a very large change to make at this time.
Which is a way of saying that a feature like this doesn't just need to argue for its usability and advantage over the status quo, it also needs to show a migration path with a cost that doesn't outweigh the benefits.
And adding the type won't remove any ambiguity over whether a function can throw an exception, not unless we also remove the ability to throw anything but errors. And then the function would still not be able to declare which exceptions it can throw, if it can throw more than one kind. If it could, it would effectively be Java's checked exceptions, just with, hopefully, nice syntax.
Just adding a Result with the expected basic operations would be enough as a first step imo. It solves the specific problem of lots of incompatible Result types proliferating on pub.dev and the ? and ! operators and must-use lints solve ergonomics issues that cannot be addressed by the community.
More isn't needed for now imo. If the community adopts this and starts using it everywhere, there could still be a discussion about migrating things or adding features to ease migration.
And adding the type won't remove any ambiguity over whether a function can throw an exception, not unless we also remove the ability to throw anything but errors.
People are okay with that compromise, considering they have already started using Result types in their projects.
If I was authoring a package with Result types, on the API borders, I'd catch all exceptions and map them to variants of my Err type.
@lrhn
And adding the type won't remove any ambiguity over whether a function can throw an exception, not unless we also remove the ability to throw anything but errors. And then the function would still not be able to declare which exceptions it can throw, if it can throw now than one kind. If it could, it would effectively be Java's checked exceptions, just with, hopefully, nice syntax.
Again, to clarify, I'm not trying to introduce checked exceptions here. That's why I was making the point about E
not needing to be throwable -- I'm fine if result!
throws some other error that says "hey look you got a failure, here's the error value: null
". So I disagree with
I think the goal of having a core
Result
class should be to remove the ambiguity of determining whether or not a method can "throw" an exception.
Maybe that's a nice direction Dart could've gone in, but by now it's a bit late to make such a change.
Result types would be for package authors and app developers to safely write code that doesn't need to throw. Maybe it's a little more work for a package author to wrap all their server calls in try/catches to get a Result type, but then all users of that package can focus on handling the error conditions the package author deemed important, instead of also blindly catching everything. This isn't to prevent Dart code from throwing anymore, it's to turn more error conditions into regular values and allow us to use expressions to handle those errors safely, rather than relying on control-based try/catch.
Also, I'd readily argue that the value of such a feature is the standardization, due to how many packages separately implement this type and are therefore incompatible.
I don't see us changing existing APIs to return a
Result<T>
rather thanT
+throwing.
The Dart SDK itself doesn't need to commit to using them, but simply exposing them to the Dart/Pub ecosystem would do wonders for compatibility of current and future packages, and allow their users to get more benefit out of them.
We (dart) could adopt syntactical sugar for result/error of or {}
block similar V lang. I could help make it less verbose.Something similar to https://github.com/vlang/v/blob/master/doc/docs.md#handling-optionsresults
Syntactic sugar is imo not important at this point; just getting a good plain old dart class / type would already be very useful. And then, once the community has adopted them it's much easier to justify discussing syntactic sugar or other language features
I'm not trying to introduce checked exceptions
How is it different ? (Assuming dart equivalent of checked exceptions would also declare string -or others- if one was thrown.)
Unlike an exception, result isn't used as a control flow. Anything beside that ?
Checked exceptions involve not being able to use throw
unless you explicitly declare it, and being forced to handle all transitively thrown exceptions or declare those as well.
A Result
type does not interfere with existing or future uses of throw
at all. It is simply an alternative, one that many packages/programming styles already use (see the "Motivation" section of this proposal). The point of standardizing a Result
class is not to enforce that the SDK use it instead of throwing, but rather to ensure that all future and current (after a little refactoring) Pub packages and private projects are compatible across different implementations (I'd also be curious if @munificent wants to scan some Pub packages to see just how many of these implementations are out there!)
So this would also allow throwing and returning results to coexist. Mostly within an API, eg, throwing Error
s on totally invalid state and returning a result type when something predictable went wrong. Ideally, we could just magically rewrite history to get rid of most throws altogether, but since that's not possible, this proposal allows existing code to remain unchanged and is, therefore, way less breaking and way more feasible than checked exceptions, whether built-in or lint-based.
The "first-class" part of this proposal would make result types even more convenient by allowing some neat built-in methods or syntax sugar to interact with result types and quickly get values out of them and handle errors. Try/catch is more restrictive and verbose, and while an inline try/catch expression would treat some of these cases, it won't treat all.
Checked exceptions involve not being able to use throw unless you explicitly declare it,
I mean, it does not exist in Dart, so there are opportunities for better design choices. Couldn't dart infer the exceptions for example ? And just hint the user that not all exceptions are caught on the other end when that's the case instead of failing to compile.
You also need to declare Result<S, E> as a return value too.
Ideally, we could just magically rewrite history to get rid of most throws altogether
Afaik, there is no consensus that this would be a good thing to begin with (and I personally don't think so). ( Way out of my depth here but maybe Djikistra hatred of go-to is the consensus ? In any case, they are convenient)
Couldn't dart infer the exceptions ? And just hint the user that not all exceptions are caught on the other end if not all are caught.
The question is where is the "other end"? Very few functions/methods in an app are called in main()
, the rest are transitively spread through classes and packages and frameworks, so where should the warning come up? To avoid breaking existing code, all warnings would appear in main()
, but to be useful, Dart would somehow need to know which errors should be handled by a package VS which are supposed to be caught by the caller. This can't really be automated without explicitly declaring them like checked exceptions do.
You also need to declare Result<S, E> as a return value too.
No because -- like you said -- there's no reason to rewrite all your throwing code in terms of Result
s. That's purely a style choice, and if you choose to migrate, you're making a breaking change. If Dart were to suddenly introduce warnings/errors for unhandled exceptions, most code today would be flagged as warnings and would thus be pretty breaking even when packages don't change versions at all.
Afaik, there is no consensus that this would be a good thing to begin with
Of course, I meant ideally from my perspective and use-case, for sure not for everyone. I even acknowledged several times that I still think Error
s should be thrown and never handled programmatically. I just meant that this proposal prioritizes compatibility with the reality that most people today just throw.
Small nitpick: in most languages I used, the "expected" return type is the first and the "error" return type is the second in the generic (Result<T, E>
).
Besides the small nitpick, this would be amazing! There's already appetite from the community to use such a Result type, and having a built in one would make it so that there aren't any clashes between different implementations.
Some of the built in operators talked about here aren't that necessary, but try
doesn't really have an easy to implement alternative. In Rust lingo, !
can just be .unwrap()
, and ?.something
can just be .map((x) => x.something)
(arguably a bit inconvenient).
Dart currently has no good way to represent an operation that can be expected to fail, besides using exceptions. For example, File.deleteSync
returns void
and the user is expected to know that the function is expected to throw. While I don't expect the standard library to adopt this feature, at least not right away, adding Result
to Dart will allow packages to start adopting this in a standardised way.
You don't need classes to return "result or error". Here's my attempt at a zero-cost implementation using extension types. Sure, it doesn't force you to check for an error (you get a runtime exception if you forget), but there's much less ceremony around the issue. (This program is not guaranteed to be correct - dartpad misbehaves on it, so please take it as a strawman)
// definitions
extension type Maybe<T>((T? v, Exception? e) pair) {
Exception? get error => pair.$2;
T get value =>
error != null ? pair.$1 as T : throw "no value, check the error";
}
Maybe<T> ok<T>(T val) => Maybe((val, null));
Maybe<Never> error(String msg) => Maybe((null, Exception(msg)));
// returning Maybe from function
Maybe<int> foo(String msg) {
return (msg == "Hello") ? ok(1) : error("wrong parameter");
}
// testing it
main() {
var x = foo("Hello");
var y=foo("");
if (x.error != null) {
throw x.error;
}
x.value;
y.value; // expected to throw
}
Is a tuple any more zero cost than a class?
My understanding is that a tuple doesn't create an object on the heap, it allocates on the stack. In terms of allocation/deallocation overhead, 2-tuple is (almost) equivalent to int. Also, all the micro-functions defined in the definition can be inlined, so it's as close to zero cost as it gets.
@tatumizer The main, most important point of this issue, is the standardization of such a type. I personally do not care if it's union types, sealed wrapper classes, extension types, records, or even a new type like String!
-- whatever gets the job done and solves the issues I laid out in the top comment, I will appreciate.
But while we can write extensions and new types ourselves, no amount of individual or third-party effort can resolve the fragmented ecosystem of packages that all try to solve the same problem in incompatible ways. Like I said in the Motivation section:
As much as this "could be handled by a package", right now the ecosystem is fragmented because it is being handled by packages, and each package is making their own, meaning the chances of any two packages being compatible are slim to none. Sure, we could make a "better" package, but that probably wouldn't help,. By making this first-party, it is more likely that other packages will contribute and migrate to the canonical implementation, increasing compatibility across the ecosystem.
As much as this "could be handled by a package", right now the ecosystem is fragmented because it is being handled by packages, and each package is making their own, meaning the chances of any two packages being compatible are slim to none.
What is exactly the problem with this? Everyone uses what they like the most. Why would we need to have a standardized Result
?
Also, what do you mean with "be compatible" regarding these packages? If I depend on A and B and each one of them uses a different Result
package, my code will still compile, unless they both export the Result
classes themselves (which would be handleable by hide
/show
, but would be cumbersome nonetheless).
Personally, I am much more fond of maybe-like monads than exceptions. IMO, exceptions should not have to exist at all. However, this is how Dart was designed and it would be massively breaking to change it now, so I just learned to live with exceptions.
Having both would be actually very confusing and fragment the community even more. I do not encourage package authors to use Result
contravariantly, and, if possible, not use it in their public API at all.
not use it in their public API at all
there are multiple packages on pub with exactly this
fragment the community even more
people are already using custom result types. standardizing one type would reduce fragmentation, not add to it.
massively breaking to change it now
this is not about removing anything; and exceptions ARE useful. this is about standardizing and simplifying the ecosystem.
What is exactly the problem with this?
people now have to write mappers to pass results between packages. results and exceptions don't really play nice together (here the language could provide some syntactial sugar or special features for interacting between the two concepts)
I just learned to live with exceptions
TLDR: good things aren't possible.
there are multiple packages on pub with exactly this
There shouldn't be. We should strive to be consistent in using exceptions instead of building an inconsistent ecosystem that uses both.
people are already using custom result types. standardizing one type would reduce fragmentation, not add to it.
Sure, use it in your apps or internally in your packages. Having a first-class Result
will encourage people to use it in their public APIs, making the ecosystem fragmented. If we all use exceptions, everything will work greatly.
this is not about removing anything; and exceptions ARE useful. this is about standardizing and simplifying the ecosystem.
This is the problem. Not removing exceptions and adding Result
has zero advantage in Dart. They are not complementary, they are two different approaches to the same problem, i.e. error handling.
This is why most languages that were designed with builtin maybe monad doesn't have an exception system.
people now have to write mappers to pass results between packages. results and exceptions don't really play nice together (here the language could provide some syntactial sugar or special features for interacting between the two concepts)
This is why we shouldn't have Result
in contravariant positions. Even better, if we all use exceptions, we will have no problem at all.
TLDR: good things aren't possible.
Yes, good things are possible. A good thing would be to remove exceptions system completely in favor of an explicit and sound maybe monad system. I would be in favor of that. But it comes with a high cost that the Dart team (understandably) don't want to pay.
Having both Result
and keep the current exception system is simply not a "good thing".
There shouldn't be. We should strive to be consistent in using exceptions instead of building an inconsistent ecosystem that uses both.
I don't agree that exceptions are the only way to handle unexpected results. For example, when you can't find a user in a database, some packages will return null while others throw. Protobuf throws errors when it can't decode a message, but otherwise never uses null to represent empty messages, instead using a system of default values. The point is, that there is no "one way" to handle every possible case of failure, and result types would just be another tool in our belts. A tool that people find themselves making from first principles quite often, because there is no standard.
Also, I'd disagree on using the collective "we" like that. My apps don't influence the existence or maintenance of package:fpdart
, and if I use their result type, I'm not influencing other packages that offer their own result types in any way. These authors and maintainers are separate from their downstream users, and there is no way to "force" everyone to migrate without breaking everything and choosing one superior option. By introducing result types as a first-class concept in Dart, everyone has a natural incentive to switch over, and no one has to worry about breaking their downstream users since the new types will be available for everyone.
Sure, use it in your apps or internally in your packages. Having a first-class
Result
will encourage people to use it in their public APIs, making the ecosystem fragmented. If we all use exceptions, everything will work greatly.
How is using a standard, first-class type "fragmenting" the ecosystem? Is using int
instead of double
problematic? How about returning sync vs future values? All types in the core SDK or first-party packages like dart:async
are free for everyone to use, and choosing to use one, like Stream
, isn't fragmentation, because it's available to everyone just as easily. On the other hand, if I want to use a result type, I wouldn't necessarily want to tie myself and my API to package:fpdart
's implementation, nor any other 3rd-party version.
This is the problem. Not removing exceptions and adding
Result
has zero advantage in Dart. They are not complementary, they are two different approaches to the same problem, i.e. error handling.
The advantage is precisely that you get another way to solve your problems. As I started off saying, exceptions are not and should not be the only way to indicate failure or lack of data, so having a type-safe and convenient alternative that forces you to consider the failure cases is something people want, and are consistently making and using.