Skip to content

pnagoorkar/fluent-results

Repository files navigation

fluent-results

Build and Test - Main

codecov

npm version

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.


✨ Why?

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.


🚀 Installation

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.


⏱ Quick start (sync)

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);
}

⏱ Quick start (async)

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);
}

🛠 API surface (sync & async)

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.


🐛 Built-in error wrappers — errors as first-class objects

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.


🛣 Detouring on contingencies (child ↔ parent)

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  ✅

🧪 Developing locally

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

🤝 Contributing

  1. Bug – open an issue & add a failing test in your PR.
  2. Feature – start with an issue so we can discuss naming and scope.

PRs are welcome!


📜 Licence

MIT – do whatever you want; attribution appreciated but not required.


🙏 Credits

Pattern & naming inspired by FluentResults for .NET.
Built with ❤️ by @pnagoorkar.

Happy fluent coding!

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published