Dart · Zero dependencies · v2.0.0

Cold, composable
computations for Dart

Jerelo gives you Cont<E, F, A> — a lazy, reusable computation with three outcome channels: success, typed errors, and crashes. Define once. Run anywhere.

$ dart pub add jerelo

Why not just Future?

Dart's Future is great for one-shot async work. But when your business logic needs to be testable, reusable, or configurable at runtime, it falls short.

Future Cont

Hot vs Cold

A Future starts executing the moment you create it. You can't store it, clone it, or defer it safely.

A Cont is cold — nothing runs until you call .run(). Define once, execute as many times as you need with different environments.

Future Cont

One Catch-All Error vs Two Dedicated Channels

Future conflates expected business errors with unexpected exceptions. Everything lands in the same catch block.

Cont keeps them separate: else carries typed business errors of type F, while crash handles unexpected exceptions. Errors are data, not exceptions.

Future Cont

Globals vs Environment Injection

Async Dart flows typically rely on global singletons or scattered constructor injection to pass config and dependencies.

The E type parameter threads an environment through every computation automatically. Use Cont.askThen() to access it anywhere in the chain.

Future Cont

Manual Parallel Wiring vs First-Class Ops

Coordinating parallel async work with Future.wait requires manual error handling and offers no control over execution policy.

Cont.all, Cont.both, Cont.either, and Cont.any compose parallel computations with configurable policies — quit-fast, sequential, or run-all.

Everything you need, nothing you don't

A small, focused API built around a single composable type. No magic, no framework — just functions that compose.

Three-Channel Design

One success channel (then) and two failure channels: else for typed business errors and crash for unexpected exceptions. Errors are data, not exceptions.

Cold Execution

No computation runs until you call .run(). The same Cont can be executed in production, in tests, and in previews — each with its own environment.

👤

Environment Injection

The E type parameter carries config, services, and dependencies without globals. Access it anywhere with Cont.askThen() and scope it with .local().

Parallel Composition

Cont.both and Cont.all merge computations. Cont.either and Cont.any race them. Configure with quit-fast, sequential, or run-all policies.

🔒

Zero Dependencies

Pure Dart. No external packages, no platform channel, no generated code. Works on Android, iOS, Web, Linux, macOS, and Windows from a single codebase.

🔄

Cooperative Cancellation

Every .run() returns a ContCancelToken. Call .cancel() to stop in-flight work cooperatively — built into the type, not bolted on.

See it in action

Real patterns from production-grade Dart code.

import 'package:jerelo/jerelo.dart';

// Build a reusable computation — nothing runs yet.
Cont<AppConfig, String, User> getUserData(String userId) {
  return fetchUserFromApi(userId)
    // Stay on 'then' only if the user is active.
    .thenIf(
      (user) => user.isActive,
      fallback: 'User account is not active',
    )
    // If API fails or user inactive, try cache (with access to env).
    .elseDoWithEnv((config, error) {
      return config.enableCache
        ? loadUserFromCache(userId)
        : Cont.error(error);
    })
    // Side effect: log access without altering the value.
    .thenTap((user) => logAccess(user))
    // Convert any unexpected exception into a typed error.
    .crashRecoverElse((crash) => 'Unexpected error: $crash');
}

// The same computation runs with different configs.
final flow = getUserData('user-42');

flow.run(prodConfig,
  onElse:  (error) => print('Error: $error'),
  onThen:  (user)  => print('Got: ${user.name}'),
);

flow.run(testConfig,
  onElse:  (error) => print('Error: $error'),
  onThen:  (user)  => print('Got: ${user.name}'),
);
import 'package:jerelo/jerelo.dart';

// Run fetch and name-validation in parallel.
// Both must succeed; fail fast on the first error.
Cont<void, String, UserProfile> fetchAndUpdate(
  String accessToken, {
  required String newName,
}) {
  return Cont.both(
    getUserProfile(accessToken),
    validateName(newName),
    (profile, validName) => profile.copyWith(name: validName),
    policy: OkPolicy.quitFast(),
  ).thenDo((merged) {
    return updateUserProfile(accessToken, merged);
  });
}

// Fetch multiple users concurrently; preserve order.
Cont<AppConfig, String, List<User>> getUsers(List<String> ids) {
  return Cont.all(
    ids.map(getUserData).toList(),
    policy: OkPolicy.quitFast(),
  );
}

// Race two strategies — normal login vs. stored refresh token.
// Return whichever succeeds first.
final loginFlow = Cont.either(
  loginRequest('user@test.com', 'password'),
  refreshTokenLogin(storedToken),
  (e1, e2) => '$e1; $e2',
  policy: OkPolicy.sequence(),
);
import 'package:jerelo/jerelo.dart';

// Build the full login-to-session flow.
final loginToSession = loginRequest('user@test.com', 'secret')
  .thenDo((token) {
    return getUserProfile(token.accessToken)
      .thenMap((profile) => Session(token: token, profile: profile));
  })
  // Log the session without changing its value.
  .thenTap((session) {
    print('[LOG] Authenticated as ${session.profile.name}');
    return Cont.of(());
  });

// Execute — all three outcome channels handled explicitly.
final token = loginToSession.run(
  null,
  // Success channel: computation produced a value.
  onThen: (session) {
    print('Welcome, ${session.profile.name}!');
  },
  // Else channel: expected business error (typed as String here).
  onElse: (error) {
    print('Login failed: $error');
  },
  // Crash channel: unexpected exception — never swallowed silently.
  onCrash: (crash) {
    print('Unexpected crash: $crash');
  },
);

// Cancel at any time — cooperative, no forced kill.
// token.cancel();

Up and running in minutes

Three steps from zero to your first composable computation.

  1. 01

    Add the dependency

    dart pub add jerelo
  2. 02

    Import it

    import 'package:jerelo/jerelo.dart';
  3. 03

    Write your first Cont

    // A cold computation — nothing runs yet.
    final greeting = Cont.of<(), Never, String>('world')
      .thenMap((name) => 'Hello, $name!');
    
    // Run it whenever, as many times as you like.
    greeting.run((), onThen: print); // Hello, world!