npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fluent-future

v1.3.2

Published

fluent future api

Readme

fluent-future

npm version

Type-safe async operations with functional composition. Like Promise but with typed errors and monadic methods.

Installation

npm install fluent-future

Problem ❌

// ❌ Native Promise: err is unknown, try/catch hell
async function loadDashboard(): Promise<Dashboard | null> {
    try {
        const user = await api.getUser()
        
        let posts: Post[]
        try {
            posts = await api.getPosts(user.id)
        } catch {
            posts = []
        }
        
        const config = await api.getConfig().catch(() => defaultConfig)
        
        return { user, posts, config }
    } catch (err) {
        console.error(err)
        return null
    }
}

Solution ✅

// Future: error type preserved, composition without nesting
const dashboard = await Bind({
  user: api.getUser(),
  config: api.getConfig().recover(defaultConfig)
})
.bind({
  posts: ({ user }) => api.getPosts(user.id).recover([])
})
.tap(({ user }) => console.log(`Hello, ${user.name}`))
.tapErr(err => console.log(err))
.recover(null)
// Future<Dashboard | null, never>

What happened:

  • Bind run getUser and getConfig in parallel
  • .bind waited for user, then fetched posts
  • .recover handled errors where they occur — not in a global catch
  • .recover(null) handled all api.get... errors across the whole chain
  • .tap ran a side effect without breaking the chain
  • ApiError type passed through the entire chain

Quick Start

import { Future, Resolve, Reject, Bind, Begin } from 'fluent-future'

// Success
const a = Resolve(42)

// Failure
const b = Reject(new ApiError(400, 'Bad Request'))

// From a Promise or function
const c = Future.of(() => JSON.parse('{"x":1}'))

// Composition
const result = await Begin<ApiError>()
  .andThen(() => api.getUser())
  .tap(user => console.log(user))
  .recoverIf(err => err.status === 404, null)

Adopt Gradually

Future is 100% Promise-compatible. Start using it as a drop-in replacement for Promise, and tap into advanced features only when you need them.

Step 1: Drop-in Replacement

// Before: native Promise
async function getUser(id: number) {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}

// After: just wrap with Future.of — everything still works
function getUser(id: number) {
  return Future.of(fetch(`/api/users/${id}`).then(r => r.json()))
}

// await works exactly the same
const user = await getUser(1)

Step 2: Use What You Need

You don't have to learn the entire API upfront. Reach for methods as your error handling grows:

// Day 1: just await it
const user = await getUser(1)

// Day 5: add a fallback
const user = await getUser(1).recover(null)

// Day 10: handle specific errors
const user = await getUser(1)
  .recoverIf(err => err.status === 404, null)
  .recoverIf(err => err.status === 403, { banned: true })

// Day 30: full composition with parallel loading
const dashboard = await Bind({
  user: getUser(1),
  config: getConfig().recover(defaultConfig)
})
.bind({
  posts: ({ user }) => getPosts(user.id).recover([])
})

Why This Matters

  • No lock-in: wrap existing Promise-returning functions, don't rewrite them
  • No big refactor: one endpoint at a time, no flag day
  • No learning cliff: your team keeps using await as usual, learns recover later
  • No breaking changes: Future is a Promise — drop it into any Promise.all or await expression

Core Concepts

Typed Errors

Future<T, E> tracks the error type alongside the success type. E survives through map, andThen, and recover — the compiler always knows what can fail.

const user: Future<User, ApiError> = api.getUser()

user
  .map(u => u.name)        // Future<string, ApiError>
  .andThen(name => ...)     // still ApiError
  .recoverIf(               // narrowed but still ApiError
    err => err.status === 404,
    null
  )

Parallel by Default

Bind and .bind run independent operations concurrently:

const data = await Bind({
  user: api.getUser(),       // }
  config: api.getConfig(),   // } these three — parallel
  flags: api.getFlags()      // }
})
.bind({
  posts: ({ user }) => api.getPosts(user.id),     // } these two — parallel,
  recs:  ({ user }) => api.getRecs(user.id)       // } wait for user
})

Declarative Error Handling

Handle errors where they happen, not in a single catch-all:

const result = await api.getUser()
  .recoverIf(err => err.status === 404, null)  // 404 → null
  .throwIf(user => user === null, new Error('User required')) // null → error
  .map(user => user.name)  // user is definitely User here

Promise Compatibility

// await works natively
const value = await Resolve(42)  // 42

// then / catch work too
Resolve(42).then(v => console.log(v))
Reject(err).catch(e => console.error(e))

API

Creation

Resolve(42)                     // Future<number>
Resolve()                       // Future<void>
Reject(new ApiError(400))       // Future<never, ApiError>
Future.of(() => JSON.parse(str))          // from function
Future.of(somePromise, err => new ApiError(err)) // with error transform
Begin<ApiError>()               // start a chain with void, error type ApiError

Context Composition

// Static Bind — creates context from independent Futures
const ctx = await Bind({
  user: api.getUser(),
  posts: api.getPosts(),
  extra: 42                    // plain values work too
})
// ctx: { user: User, posts: Post[], extra: number }

// .bind — extends context with access to previous fields
const full = await Bind({ user: api.getUser() })
  .bind({
    posts: ({ user }) => api.getPosts(user.id),
    recs:  ({ user }) => api.getRecommendations(user.id)
  })
  .bind({
    feed: ({ posts, recs }) => mergeFeed(posts, recs)
  })

Transformations

future.map(value => transform(value))     // change success, preserve error
future.mapErr(err => new ApiError(err))   // change error, preserve success
future.andThen(value => anotherFuture())  // chain: success → new Future
future.orElse(err => fallbackFuture())    // chain: error → new Future

Error Handling

// Error → Success
future.recover(defaultValue)              // all errors → value
future.recoverIf(predicate, fallback)     // matching errors → value

// Success → Error
future.throw(errorValue)                  // all success → error
future.throwIf(predicate, errorValue)     // matching success → error

Side Effects

future.tap(value => console.log(value))   // log success, chain unchanged
future.tapErr(err => sentry.capture(err)) // log error, chain unchanged
future.finally(() => setLoading(false))   // cleanup regardless of outcome

Unwrapping

await future.unwrap()                     // T — throws on failure
await future.unwrapOr(default)            // T | default — no throw
await future.unwrapOrElse(err => fallback) // T | fallback — from function
await future.expect('User load failed')   // T — with custom error message
await future.isOk()                       // boolean
await future.isErr()                      // boolean

await future.match({
  ok: value => `Got ${value}`,
  err: error => `Failed: ${error}`
})

Static Methods

Future.all([a, b, c])     // wait for all, fail if any fails
Future.any([a, b, c])     // first success, AggregateError if all fail
Future.race([a, b, c])    // first to complete (success or failure)
Future.isFuture(obj)      // type guard

Real-World Examples

Dashboard Load

const dashboard = await Bind({
  user: api.getUser(),
  config: api.getConfig().recover(defaultConfig),
  announcements: api.getAnnouncements().recover([])
})
.bind({
  posts: ({ user }) => api.getPosts(user.id).recover([]),
  notifications: ({ user }) => api.getNotifications(user.id).recover([])
})
.bind({
  feed: ({ posts, announcements }) => [...posts, ...announcements]
})
.tap(({ user }) => analytics.track('dashboard_loaded', { userId: user.id }))
// Future<Dashboard, ApiError>

Form with Dependent Lookups

const formData = await Bind({
  categories: api.getCategories(),
  countries: api.getCountries()
})
.bind({
  cities: ({ countries }) => api.getCities(countries[0].id),
  subcategories: ({ categories }) => api.getSubcategories(categories[0].id)
})
// Future<FormData, ApiError>

404 → null, Propagate Other Errors

const user = await api.getUser()
  .recoverIf(err => err.status === 404, null)
// Future<User | null, ApiError>
// 404 becomes null, everything else stays as error

Chain with Fallback

const data = await api.getPrimary()
  .orElse(() => api.getSecondary())
  .orElse(() => api.getCached())
  .recover(defaultValue)
// Tries primary → secondary → cached → default
// Never fails — Future<T, never>

Comparison

| | Future | Native Promise | |---|---|---| | Typed errors | ✅ Future<T, E> | ❌ Promise<T> (err: unknown) | | Parallel composition | ✅ Bind / .bind | ⚠️ Promise.all + manual destructure | | Error recovery | ✅ recover / recoverIf | ⚠️ .catch() | | Success → error | ✅ throw / throwIf | ❌ manual throw | | Side effects | ✅ tap / tapErr / finally | ⚠️ manual | | Pattern matching | ✅ match | ❌ | | await compatible | ✅ native await | ✅ native |