A tiny, dependency‑free generic helper for composing success/failure pipelines in JavaScript & TypeScript; with first‑class sync & async fluent APIs. Heavily inspired by the .NET FluentResults pattern.
JavaScript exceptions work, but once business logic starts branching, nested try / catch
blocks become hard to follow. fluent‑results lets you:
- Compose operations fluently (chain calls like
.bind(…)
). - Keep data and errors together in a single object.
- Short‑circuit automatically after the first failure.
- Pass the current state between steps without global variables.
- Mix synchronous and Promise‑returning steps in the same chain.
- Stay on the happy path while you can and swith to a contingent path when you can't.
If you enjoy functional pipelines or railway‑oriented programming, you’ll feel at home.
npm i fluent-results
# or
yarn add fluent-results
The package ships ES modules and .d.ts
typings so TypeScript and plain‑JavaScript editors get IntelliSense out‑of‑the‑box.
import { Result } from 'fluent-results';
const result =
Result.try(() => 42) // start a pipeline – succeeds, state=42
.bind(n => n + 1) // state becomes 43
.okIf(state => state < 50, // predicate true → stays Success
new MyError('Too big'))
.bind(() => {
throw new Error('Boom'); // failure captured, pipeline stops
});
if (result.isSuccess) {
console.log('Yay 🎉', result.currentState);
} else {
console.error('Failed:', result.errors);
}
const userResult = await Result.tryAsync(() => fetch('/user/7').then(r => r.json())) // Result<User>
.then(result => result.okIfAsync(u => u.active, new AuthError('Inactive user')) // guard
.then(result => result.bindAsync(user => saveAudit(user.id))); // Result<AuditRow>
if (userResult.isSuccess) {
console.log('Audit id:', userResult.currentState.id);
} else {
console.error(userResult.errors);
}
Member | Purpose | Example |
---|---|---|
Result.try(action, routineName) |
Kick off a sync pipeline; thrown exceptions are wrapped in ExceptionalError . |
Result.try(() => doWork(), "My primary routine") |
Result.tryAsync(action, routineName) |
Kick off an async pipeline; rejected promises become PromiseRejection . |
Result.tryAsync(() => fetch('/api').then(r => r.json()), "My primary routine") |
.bind(fn) |
Chain a synchronous step; skipped when the pipeline is already failed. | .bind(n => n + 1) |
.bindAsync(fn) |
Chain an async step that returns a Promise. | .bindAsync(async n => n * 2) |
.okIf(pred, err) |
Keep success only if predicate returns true ; otherwise push err . |
.okIf(x => x < 50, new RangeError()) |
.okIfAsync(pred, err) |
Async predicate version. | .okIfAsync(async x => await isValid(x), new ValidationError()) |
.failIf(pred, err, contingency) |
Turn pipeline into failure only if predicate returns true . Optionally define a contingent routine that will be execute if the predicate evaluates to true |
.failIf(() => isDuplicate(), new ConflictError()) |
.failIfAsync(pred, err, contingency) |
Async predicate version. Optionally define a contingent routine that will be execute if the predicate evaluates to true | .failIfAsync(async () => await exists(), new ConflictError()) |
Getters | Inspect pipeline state and trace execution path | result.isSuccess , result.currentState , result.errors , result.child , result.parent |
Async variants return Promise<Result<…>>
; you can freely interleave them with the sync ones.
A big advantage of the fluent-results pattern is that every failure is represented by a real object instead of a brittle string or numeric code.
That lets you pattern-match on error types, attach extra fields (status codes, correlation IDs, etc.), and serialize the whole Result
for telemetry - while keeping the pipeline fluent.
Wrapper class | Added when… | Typical use |
---|---|---|
ExceptionalError |
A synchronous delegate throws. | Preserve the original stack trace & message while converting the exception into a typed reason the pipeline understands. |
PromiseRejection |
A Promise inside tryAsync , bindAsync , okIfAsync , or failIfAsync rejects. |
Surface async failures in exactly the same, type-safe way as sync ones. |
Because failures are objects, you can easily branch on domain-specific subclasses:
const result = await Result.tryAsync(() => fetch("/myControlledAccessEndpoint"));
.then(result => result.failIf(response => response.status === 403, new AuthError(response)))
.then(result => result.bind(...))
.then(result => result.okIf(...))
.then(result => result.bind(...));
if (result.isFailed) {
const authIssues = result.errors.filter(e => e instanceof AuthError);
if (authIssues.length) {
console.log(`user not authorized to access endpoint: ${authIssues[0].response.url}`);
}
}
Extend AError
to introduce your own domain‑specific failures.
Stay on the happy path while you can; continue executing on a contingent path if you can't
const result = Result.try(() => loadCache(), "Load from cache")
.failIf(cache => cache === null, // contingency trigger
new CacheMissError(), // error pushed onto the main Result
{func: fetchFromServer, routineName: "Load from server"}) // contingency
.bind(cache => cache.getData()); // will never run
const data = result.isSuccess ? result.currentState : result.child.currentState;
// ── relationships ─────────────────────────────────────────────
// result.child → Result fetched from server
// result.child.parent === result ✅
git clone https://github.com/pnagoorkar/fluent-results.git
cd fluent-results
npm ci # exact dev dependencies
npm test # Jest suite
npm run build # emits ES modules + type declarations into /dist
- Bug – open an issue & add a failing test in your PR.
- Feature – start with an issue so we can discuss naming and scope.
PRs are welcome!
MIT – do whatever you want; attribution appreciated but not required.
Pattern & naming inspired by FluentResults for .NET.
Built with ❤️ by @pnagoorkar.
Happy fluent coding!