本页尚未翻译,因此以英文显示。 参与翻译

Typed errors

Write straight-line code that fails with a typed error. The approach of Kotlin's Arrow 2.x, ported to Dart.

Either<E, A> either<E, A>(A Function(Raise<E> r) block) Future<Either<E, A>> eitherAsync<E, A>(FutureOr<A> Function(Raise<E> r) block) A? nullable<A>(A Function(SingletonRaise r) block)
Deep dives. This page is the overview; every subject has a detailed tutorial with runnable demos: Either · either & the Raise scope · nullable · NonEmptyList · accumulation · Either × pipelines

From Kotlin Arrow to Dart

In Kotlin Arrow, an either { } block turns a chain of fallible steps into straight-line code — each .bind() either unwraps a success or short-circuits the whole block with the failure:

// Kotlin Arrow
fun getResult(): Either<Failure, SuccessData> = either {
    val user  = findUser(userId).bind()
    val order = findOrder(user.id).bind()
    val total = calculateTotal(order).bind()
    SuccessData(user, order, total)
}

FxDart gives you the same shape in Dart:

// FxDart
Either<Failure, SuccessData> getResult() => either((r) {
  final user  = r.bind(findUser(userId));
  final order = r.bind(findOrder(user.id));
  final total = r.bind(calculateTotal(order));
  return SuccessData(user, order, total);
});

Both replace the nested flatMap pyramid you would otherwise write:

// What it replaces
Either<Failure, SuccessData> getResult() =>
    findUser(userId).flatMap((user) =>
        findOrder(user.id).flatMap((order) =>
            calculateTotal(order).map((total) =>
                SuccessData(user, order, total))));

The two differences from Kotlin are Dart realities: the scope is an explicit parameter (r) because Dart has no lambda receivers, and async has its own builder (eitherAsync) because Dart has no inline. Internally this is not flatMap chaining: like Arrow, r.bind on a failure throws a private, scope-tagged signal that the builder catches at the boundary — which is why early returns, loops, and ifs all just work inside the block, and why nested builders never capture each other's errors.

Deep dive: Either

The scope vocabulary

Everything hangs off the r the builder hands you — type r. and discover it all:

Either<String, int> parsePort(String raw) => either((r) {
  final n = r.ensureNotNull(int.tryParse(raw), () => '"$raw" is not a number');
  r.ensure(n > 0 && n < 65536, () => '$n is out of range');
  return n;
});

switch (parsePort('8080')) {
  case Right(:final value): print('listening on $value');
  case Left(:final value):  print('bad config: $value');
}

eitherAsync is the async twin (raise only in the same awaited chain); nullable/nullableAsync are the nullable-first twins that return T? instead of an Either — FxDart stays nullable-first, so there is no Option type.

Deep dive: either & the Raise scope → · Deep dive: nullable

Accumulate every failure, not just the first

Validation wants all the errors, not the first one. This is Arrow's replacement for a separate Validated type:

final user = either<Nel<String>, User>((r) => r.accumulate((acc) {
  final name = acc.accumulating((r) => validateName(r, input));
  final age  = acc.accumulating((r) => validateAge(r, input));
  return User(name.value, age.value); // all errors reported together
}));

r.accumulate runs every branch and concatenates all failures into a NonEmptyList (Nel) — a zero-cost extension type that cannot be empty. Fixed-arity conveniences r.zipOrAccumulate2..5 cover the common cases, and r.mapOrAccumulate(items, transform) validates a whole collection fail-slow. r.bindNel lets one branch contribute several errors at once; someEither.toEitherNel() bridges a fail-fast value into an accumulating scope.

Deep dive: accumulation → · Deep dive: NonEmptyList

Fused with pipelines

This is the part neither Arrow nor any Dart FP library has: typed errors fused with FxDart's lazy, concurrency-aware pipelines.

// Validate 500 records, 8 at a time, and keep EVERY failure — in order.
final result = await fxStream(records)
    .mapOrAccumulate<String, User>((r, rec) async {
  final parsed = r.ensureNotNull(tryParse(rec), () => 'bad record: $rec');
  return await enrich(parsed);
}, concurrency: 8);

rights(), lefts(), separated(), sequence() (fail-fast, stops pulling at the first Left) and mapOrAccumulate() (fail-slow) are eager terminals on fx()/async chains. Concurrent validation rides the same concurrent(n) back-channel as the rest of FxDart; each element runs in its own scope, so a failure in one element can never leak into a sibling.

Deep dive: Either × pipelines →

Exceptions vs raised errors

The boundary is hard: raised errors are your domain's typed failures; thrown exceptions are defects, and they propagate out of either untouched. To capture a throw into an Either, be explicit:

final parsed = Either.catching(() => jsonDecode(raw));       // Either<Object, dynamic>
final typed  = Either.catchingWith(ParseFailure.new, () => jsonDecode(raw));

Deep dive: Either.catching lives on the Either page →

Two rules. (1) Never return a lazy pipeline from a raise block — materialize with toList() or use the eager terminals above; a deferred raise fails loudly with RaiseLeakedError. (2) Never bare-catch inside a raise block — use catching/catchingAsync, which always let the short-circuit signal through (on Exception is already safe: the signal is an Error).

Curious why this page is called typed errors instead of a functional-programming keyword like Monad? The naming rationale →