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
The Problem
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.
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 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.
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.
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.
What You Get
A small, focused API built around a single composable type. No magic, no framework — just functions that compose.
One success channel (then) and two failure channels: else for typed business errors
and crash for unexpected exceptions. Errors are data, not exceptions.
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.
The E type parameter carries config, services, and
dependencies without globals. Access it anywhere with
Cont.askThen() and scope it with .local().
Cont.both and Cont.all merge computations.
Cont.either and Cont.any race them.
Configure with quit-fast, sequential, or run-all policies.
Pure Dart. No external packages, no platform channel, no generated code. Works on Android, iOS, Web, Linux, macOS, and Windows from a single codebase.
Every .run() returns a ContCancelToken.
Call .cancel() to stop in-flight work cooperatively —
built into the type, not bolted on.
In Practice
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();
Quick Start
Three steps from zero to your first composable computation.
dart pub add jerelo
import 'package:jerelo/jerelo.dart';
// 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!