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!