@flex-development/when
v3.0.0
Published
Chain callbacks on synchronous values or thenables without forcing Promise.resolve or microtasks.
Downloads
676
Maintainers
Readme
:timer_clock: when
like .then, but for synchronous values and thenables.
Contents
- What is this?
- When should I use this?
- Install
- Use
- API
- Testing
- Types
Awaitable<T>Catch<[T][, Reason]>Catchable<[T]>Chain<[T][, Next][, Args][, This]>CreateThenableOptionsExecutor<[T][, Reason]>Fail<[Next][, Reason][, This]>Finalizable<[T]>Finally<[T]>Finish<[This]>OnFinallyOnFulfilled<T[, Next]>OnRejected<Next[, Reason]>Options<[T][, Next][, Failure][, Args][, Error][, This]>PromiseLike<T>Reject<[Reason]>Resolve<[T]>Then<[T][, Reason]>Thenable<[T]>
- Glossary
- Project
What is this?
when is a tiny primitive for chaining callbacks
onto awaitables (synchronous or thenable values).
For thenable values, then is used to invoke the callback after resolution. Otherwise, the callback fires immediately.
This makes it easy to write one code path that supports both synchronous values and promises.
When should I use this?
when is especially useful in libraries implementing awaitable APIs.
It provides Promise.prototype.then semantics without forcing Promise.resolve,
preserving synchronous execution whenever possible.
Typical use cases include plugin systems, hook pipelines, module resolvers, data loaders, and file system adapters where users may return both synchronous or asynchronous values.
If you're only dealing with promises and thenables, consider using native async/await or Promise chaining instead.
Why not Promise.resolve?
when preserves synchronous operations whenever possible,
avoiding unnecessary promise allocation and microtask scheduling.
Promise.resolve(value).then(fn) // always a promisewhen(value, fn) // only a promise if `value` is a thenable, or `fn` returns oneDesign guarantees
- Synchronous values remain synchronous
- Thenables are chained directly without wrapping in
Promise.resolve - No additional microtasks are scheduled for non-thenables
- Failures propagate unless a
failhandler is provided
Install
This package is ESM only.
In Node.js (version 20+) with yarn:
yarn add @flex-development/whenIn Deno with esm.sh:
import { when } from 'https://esm.sh/@flex-development/when'In browsers with esm.sh:
<script type="module">
import { when } from 'https://esm.sh/@flex-development/when'
</script>Use
Chain a synchronous value
import { isThenable, when } from '@flex-development/when'
import { ok } from 'devlop'
/**
* The result.
*
* @const {number} result
*/
const result: number = when(0, n => n + 1)
ok(!isThenable(result), 'expected `result` to not be thenable')
console.dir(result) // 1Chain a thenable
import { isPromise, when } from '@flex-development/when'
import { ok } from 'devlop'
/**
* The result.
*
* @const {Promise<number>} result
*/
const result: Promise<number> = when(Promise.resolve(2), n => n + 1)
ok(isPromise(result), 'expected `result` to be a promise')
console.dir(await result) // 3Pass arguments to the chain callback
When arguments are provided, they are passed to the chain callback first, followed by the resolved value.
When the value passed to when is not a thenable, the resolved value is the same value.
import when from '@flex-development/when'
/**
* The result.
*
* @const {number} result
*/
const result: number = when(
1, // last argument passed to `Math.min`
Math.min, // `chain`
null, // `fail`
undefined, // `context`
2, // first argument passed to `Math.min`
3, // second argument passed to `Math.min`
4 // third argument passed to `Math.min`
)
console.dir(result) // 1Handle failures
For thenables, the fail callback is passed to then as the onrejected parameter,
and if implemented, to catch as well to prevent unhandled rejections.
import when from '@flex-development/when'
/**
* The thenable value.
*
* @const {PromiseLike<never>} value
*/
const value: PromiseLike<never> = new Promise((resolve, reject) => {
return void reject(new Error('nope', { cause: { url: import.meta.url } }))
})
/**
* The result.
*
* @const {Promise<boolean>} result
*/
const result: Promise<boolean> = when(value, chain, fail)
console.dir(await result) // false
/**
* @this {void}
*
* @return {true}
* The success result
*/
function chain(this: void): true {
return true
}
/**
* @this {void}
*
* @param {Error} e
* The error to handle
* @return {false}
* The failure result
*/
function fail(this: void, e: Error): false {
return console.dir(e), false
}Bind this context
import when from '@flex-development/when'
/**
* The `this` context.
*/
type Context = { prefix: string }
/**
* The result.
*
* @const {string} result
*/
const result: string = when(13, id, null, { prefix: 'id:' })
console.log(result) // 'id:13'
/**
* @this {Context}
*
* @param {number | string} num
* The id number
* @return {string}
* The id string
*/
function id(this: Context, num: number | string): string {
return this.prefix + num
}Use an options object
/**
* The `this` context.
*/
type Context = { errors: Error[] }
/**
* The thenable value.
*
* @const {Promise<number>} value
*/
const value: Promise<number> = new Promise(resolve => resolve(3))
/**
* The result.
*
* @const {Promise<number | undefined>} result
*/
const result: Promise<number | undefined> = when(value, {
args: [39],
chain: divide,
context: { errors: [] },
fail
})
console.dir(await result) // 13
/**
* @this {void}
*
* @param {number} dividend
* The number to divide
* @param {number} divisor
* The number to divide by
* @return {number}
* The quotient
*/
function divide(this: void, dividend: number, divisor: number): number {
return dividend / divisor
}
/**
* @this {Context}
*
* @param {Error} e
* The error to handle
* @return {undefined}
*/
function fail(this: Context, e: Error): undefined {
return void this.errors.push(e)
}Integrate with @typescript-eslint
/**
* The eslint configuration.
*
* @type {import('eslint').Linter.Config[]}
* @const config
*/
const config = [
{
files: ['**/*.+(cjs|cts|js|jsx|mjs|mts|ts|tsx)'],
rules: {
'@typescript-eslint/promise-function-async': [
2,
{
allowedPromiseNames: ['Thenable']
}
]
}
}
]
export default configAPI
when exports the identifiers listed below.
The default export is when.
isCatchable<T>(value)
Check if value looks like a Thenable that can be caught.
Type Parameters
T(any) — the resolved value
Parameters
value(unknown) — the thing to check
Returns
(value is Catchable<T>)
true if value is a thenable with a catch method, false otherwise
isFinalizable<T>(value)
Check if value looks like a Thenable that can be finalized.
Type Parameters
T(any) — the resolved value
Parameters
value(unknown) — the thing to check
Returns
(value is Finalizable<T>)
true if value is a thenable with a finally method, false otherwise
isPromise<T>(value[, finalizable])
Check if value looks like a Promise.
👉 Note: This function intentionally performs structural checks instead of brand checks. It does not rely on
instanceof Promiseor constructors, making it compatible with cross-realm promises and custom thenables.
Type Parameters
T(any) — the resolved value
Parameters
value(unknown) — the thing to checkfinalizable(boolean|null|undefined) — whether afinallymethod is required.
whenfalse, onlythenandcatchare checked
Returns
(value is Promise<T>)
true if value is a thenable with a catch method,
and finally method (if requested), false otherwise
isPromiseLike<T>(value)
Check if value looks like a PromiseLike structure.
Type Parameters
T(any) — the resolved value
Parameters
value(unknown) — the thing to check
Returns
(value is PromiseLike<T>)
true if value is an object or function with a then method, false otherwise
isThenable<T>(value)
Check if value looks like a thenable.
Type Parameters
T(any) — the resolved value
Parameters
value(unknown) — the thing to check
Returns
(value is Thenable<T>) true if value is an object or function with a then method,
and maybe-callable methods catch and/or finally, false otherwise
when<T[, Next][, Failure][, Args][, Error][, This][, Result]>(value, chain[, fail][, context][, ...args])
Chain a callback, calling the function after value is resolved,
or immediately if value is not a thenable.
Overloads
function when<
T,
Next = any,
Args extends any[] = any[],
This = unknown,
Result extends Awaitable<Next> = Awaitable<Next>
>(
this: void,
value: Awaitable<T>,
chain: Chain<T, Next, Args, This>,
fail?: null | undefined,
context?: This | null | undefined,
...args: Args
): Resultfunction when<
T,
Next = any,
Failure = Next,
Args extends any[] = any[],
Error = any,
This = unknown,
Result extends Awaitable<Failure | Next> = Awaitable<Failure | Next>
>(
this: void,
value: Awaitable<T>,
chain: Chain<T, Next, Args, This>,
fail?: Fail<Failure, Error, This> | null | undefined,
context?: This | null | undefined,
...args: Args
): Resultfunction when<
T,
Next = any,
Failure = Next,
Args extends any[] = any[],
Error = any,
This = unknown,
Result extends Awaitable<Failure | Next> = Awaitable<Failure | Next>
>(
this: void,
value: Awaitable<T>,
chain: Options<T, Next, Failure, Args, Error, This>
): ResultType Parameters
T(any) — the previously resolved valueNext(any, optional) — the next resolved value- default:
any
- default:
Failure(any, optional) — the next resolved value on failure- default:
Next
- default:
Args(readonly any[], optional) — the chain function arguments- default:
any[]
- default:
Error(any, optional) — the error to possibly handle- default:
any
- default:
This(any, optional) — thethiscontext- default:
unknown
- default:
Result(Awaitable<Failure | Next>, optional) — the next awaitable- default:
Awaitable<Failure | Next>
- default:
Parameters
value(Awaitable<T>) — the current awaitablechain(Chain<T, Next, Args, This>|Options<T, Next, Failure, Args, Error, This>) — the chain callback or options for chainingfail(Fail<Failure, Error, This>|null|undefined) — the callback to fire when a failure occurs. failures include:- rejections of the input thenable
- rejections returned from
chain - synchronous errors thrown in
chain
if nofailhandler is provided, failures are re-thrown or re-propagated.
👉 note: for thenables, this callback is passed to
thenas theonrejectedparameter, and if implemented, tocatchas well to prevent unhandled rejections.context(This|null|undefined) — thethiscontext of the chain andfailcallbacks...args(Args) — the arguments to pass to the chain callback
Returns
(Awaitable<Failure | Next> | Awaitable<Next>) The next awaitable
Testing
Test utilities are exported from @flex-development/when/testing.
There is no default export.
import {
isCatchable,
isFinalizable,
type Thenable
} from '@flex-development/when'
import { createThenable } from '@flex-development/when/testing'
import { ok } from 'devlop'
/**
* The thenable.
*
* @const {Thenable<number>} thenable
*/
const thenable: Thenable<number> = createThenable(resolve => resolve(10))
ok(isCatchable(thenable), 'expected `thenable` to be a catchable')
ok(isFinalizable(thenable), 'expected `thenable` to be a finalizable')
console.dir(await thenable.then(value => value + 3)) // 13createThenable<T[, Reason][, Result]>(executor[, options])
Create a thenable.
The returned object conforms to Thenable and ensures then always returns another Thenable,
even when adopting a foreign thenable.
When options is omitted, null, or undefined, the returned thenable is modern (a thenable
with then, catch, and finally methods).
Pass an options object (e.g. {}) to start from a bare (then method only) thenable
and selectively enable methods.
Type Parameters
T(any) — the resolved valueReason(any, optional) — the reason for a rejection- default:
Error
- default:
Result(Thenable<T>, optional) — the thenable- default:
Thenable<T>
- default:
Parameters
executor(Executor<T, Reason>) — the initialization callbackoptions(CreateThenableOptions|null|undefined, optional) — options for creating a thenable
Returns
(Result) The thenable
Types
This package is fully typed with TypeScript.
Awaitable<T>
A synchronous or thenable value (type).
type Awaitable<T> = Thenable<T> | TType Parameters
T(any) — the resolved value
Catch<[T][, Reason]>
Attach a callback only for the rejection of a Thenable (type).
type Catch<T = unknown, Reason = any> = <Next = never>(
this: any,
onrejected?: OnRejected<Next, Reason> | null | undefined
) => Thenable<Next | T>Type Parameters
T(any, optional) — the resolved value- default:
unknown
- default:
Reason(any, optional) — the reason for the rejection- default:
any
- default:
Next(any, optional) — the next resolved value- default:
never
- default:
Parameters
onrejected(OnRejected<Next, Reason>|null|undefined) — the callback to execute when the thenable is rejected
Returns
(Thenable<Next | T>) The next thenable
Catchable<[T]>
A Thenable that can be caught (interface).
Extends
Type Parameters
T(any, optional) — the resolved value- default:
any
- default:
Properties
catch(Catch<T>) — attach a callback only to be invoked on rejection
Chain<[T][, Next][, Args][, This]>
A chain callback (type).
type Chain<
T = any,
Next = any,
Args extends readonly any[] = any[],
This = unknown
> = (this: This, ...params: [...Args, T]) => Awaitable<Next>Type Parameters
T(any, optional) — the previously resolved value- default:
any
- default:
Next(any, optional) — the next resolved value- default:
any
- default:
Args(readonly any[], optional) — the function arguments- default:
any[]
- default:
This(any, optional) — thethiscontext- default:
unknown
- default:
Parameters
this(This)...params([...Args, T]) — the function parameters, with the last being the previously resolved value. in cases where a promise is not being resolved, this is the samevaluepassed towhen
Returns
(Awaitable<Next>) The next awaitable
CreateThenableOptions
Options for creating a thenable (interface).
👉 Note: Exported from
@flex-development/when/testingonly.
Properties
catch?(boolean|null|undefined) — control whether returned thenables implement acatchmethod.
when an options object is omitted,null, orundefined, the method will be implemented.<br/> when an options object is provided,catchis only implemented ifoptions.catchistrue.
ifoptions.catchisnullorundefined, the thenable'scatchproperty will have the same value.
passfalseto disable the method implementationfinally?(boolean|null|undefined) — control whether returned thenables implement afinallymethod.
when an options object is omitted,null, orundefined, the method will be implemented.
when an options object is provided,finallyis only implemented ifoptions.finallyistrue.
ifoptions.finallyisnullorundefined, the thenable'sfinallyproperty will have the same value.
passfalseto disable the method implementation
Executor<[T][, Reason]>
The callback used to initialize a thenable (type).
👉 Note: Exported from
@flex-development/when/testingonly.
type Executor<T = any, Reason = Error> = (
this: void,
resolve: Resolve<T>,
reject: Reject<Reason>
) => undefined | voidType Parameters
T(any, optional) — the resolved value- default:
any
- default:
Reason(any, optional) — the reason for a rejection- default:
Error
- default:
Parameters
resolve(Resolve<T>) — the callback used to resolve the thenable with a value or the result of another awaitablereject(Reject<Reason>) — the callback used to reject the thenable with a provided reason or error
Returns
(undefined | void) Nothing
Fail<[Next][, Reason][, This]>
The callback to fire when a failure occurs (type).
type Fail<
Next = any,
Reason = any,
This = unknown
> = (this: This, reason: Reason) => Awaitable<Next>Type Parameters
Next(any, optional) — the next resolved value- default:
any
- default:
Reason(any, optional) — the reason for the failure- default:
any
- default:
This(any, optional) — thethiscontext- default:
unknown
- default:
Parameters
this(This)reason(Reason) — the reason for the failure
Returns
(Awaitable<Next>) The next awaitable
Finalizable<[T]>
A Thenable that can be finalized (interface).
Extends
Type Parameters
T(any, optional) — the resolved value- default:
any
- default:
Properties
finally(Finally<T>) — attach a callback only to be invoked on settlement (fulfillment or rejection)👉 note: the resolved value cannot be modified from the callback
Finally<[T]>
Attach a callback that is invoked only when a Thenable is settled (fulfilled or rejected) (type).
type Finally<T = unknown> = (
this: any,
onfinally?: OnFinally | null | undefined
) => Thenable<T>Type Parameters
T(any, optional) — the resolved value- default:
unknown
- default:
Parameters
onfinally(OnFinally|null|undefined) — the callback to execute when the thenable is settled
Returns
(Thenable<T>) The next thenable
Finish<[This]>
A post-processing hook invoked exactly once after an awaitable settles,
regardless of success or failure (type).
The resolved value cannot be modified from the hook, and any error is re-thrown after execution.
type Finish<This = unknown> = (this: This) => undefined | voidType Parameters
This(any, optional) — thethiscontext- default:
unknown
- default:
Returns
(undefined | void) Nothing
OnFinally
The callback to execute when a Thenable is settled (fulfilled or rejected) (type).
type OnFinally = (this: unknown) => undefined | voidReturns
(undefined | void) Nothing
OnFulfilled<T[, Next]>
The callback to execute when a Thenable is resolved (type).
type OnFulfilled<T, Next = T> = (this: unknown, value: T) => Awaitable<Next>Type Parameters
T(any) — the resolved valueNext(any, optional) — the next resolved value- default:
T
- default:
Parameters
value(T) — the resolved value
Returns
(Awaitable<Next>) The next awaitable
OnRejected<Next[, Reason]>
The callback to execute when a Thenable is rejected (type).
type OnRejected<
Next,
Reason = any
> = (this: unknown, reason: Reason) => Awaitable<Next>Type Parameters
Next(any, optional) — the next resolved value- default:
any
- default:
Reason(any, optional) — the reason for the rejection- default:
any
- default:
Parameters
reason(Reason) — the reason for the rejection
Returns
(Awaitable<Next>) The next awaitable
Options<[T][, Next][, Failure][, Args][, Error][, This]>
Options for chaining (interface).
interface Options<
T = any,
Next = any,
Failure = Next,
Args extends readonly any[] = any[],
Error = any,
This = any
> { /* ... */ }Type Parameters
T(any, optional) — the previously resolved value- default:
any
- default:
Next(any, optional) — the next resolved value- default:
any
- default:
Failure(any, optional) — the next resolved value on failure- default:
Next
- default:
Args(readonly any[], optional) — the chain function arguments- default:
any[]
- default:
Error(any, optional) — the error to possibly handle- default:
any
- default:
This(any, optional) — thethiscontext- default:
any
- default:
Properties
args?(Args|null|undefined) — the arguments to pass to thechaincallbackchain(Chain<T, Next, Args, This>) — the chain callbackcontext?(This|null|undefined) — thethiscontext of thechainandfailcallbacksfail?(Fail<Next, Error, This>|null|undefined) — the callback to fire when a failure occurs. failures include:- rejections of the input thenable
- rejections returned from
chain - synchronous errors thrown in
chain
if nofailhandler is provided, failures are re-thrown or re-propagated.
👉 note: for thenables, this callback is passed to
thenas theonrejectedparameter, and if implemented, tocatchas well to prevent unhandled rejections.finish?(Finish<This>|null|undefined) — the callback to invoke after chaining completes, whether the operation succeeds or fails.
it runs exactly once afterchainandfail, cannot affect the resolved value, and does not intercept errors
PromiseLike<T>
To ensure native Promise and PromiseLike are assignable to Thenable,
when ships a small global augmentation for PromiseLike.
No new methods or overloads are introduced — the then signature is rewritten to match
the official TypeScript lib definition (as in lib.es2015.d.ts).
This is required for both compatibility, and type inference when mixing Thenable with built-in promise types.
Type Parameters
T(any) — the resolved value
Reject<[Reason]>
The callback used to reject a thenable with a provided reason or error (type).
👉 Note: Exported from
@flex-development/when/testingonly.
type Reject<Reason = Error> = (this: void, reason: Reason) => undefinedType Parameters
Reason(any, optional) — the reason for the rejection- default:
Error
- default:
Parameters
reason(Reason) — the reason for the rejection
Returns
(undefined) Nothing
Resolve<[T]>
The callback used to resolve a thenable with a value
or the result of another awaitable (type).
👉 Note: Exported from
@flex-development/when/testingonly.
type Resolve<T = any> = (this: void, value: Awaitable<T>) => undefinedType Parameters
T(any, optional) — the resolved value- default:
any
- default:
Parameters
value(Awaitable<T>) — the awaitable
Returns
(undefined) Nothing
Then<T[, Reason]>
Attach callbacks for the resolution and/or rejection of a Thenable (type).
type Then<T = unknown, Reason = any> = <Succ = T, Fail = never>(
this: any,
onfulfilled?: OnFulfilled<T, Succ> | null | undefined,
onrejected?: OnRejected<Fail, Reason> | null | undefined
) => Thenable<Fail | Succ>Type Parameters
T(any, optional) — the previously resolved value- default:
unknown
- default:
Reason(any, optional) — the reason for a rejection- default:
any
- default:
Succ(any, optional) — the next resolved value on success- default:
T
- default:
Fail(any, optional) — the next resolved value on failure- default:
never
- default:
Parameters
onfulfilled(OnFulfilled<T, Succ>|null|undefined) — the callback to execute when the thenable is resolvedonrejected(OnRejected<Fail, Reason>|null|undefined) — the callback to execute when the thenable is rejected
Returns
(Thenable<Fail | Succ>) The next thenable
Thenable<[T]>
The completion of an asynchronous operation, and the minimal structural contract required
by when to treat a value as asynchronous (interface).
Unlike PromiseLike, this interface allows a maybe-callable catch method, which when present,
is used by when to ensure failures are handled without forcing promise allocation.
Maybe-callable methods are named so because they are not required,
and may be a method implementation, null, or undefined.
Type Parameters
T(any, optional) — the resolved value- default:
any
- default:
Properties
catch?(Catch<T>|null|undefined) — attach a callback only to be invoked on rejectionthen(Then<T>) — attach callbacks to be invoked on resolution (fulfillment) and/or rejectionfinally?(Finally<T>|null|undefined) — attach a callback only to be invoked on settlement (fulfillment or rejection)👉 note: the resolved value cannot be modified from the callback
Glossary
awaitable
A synchronous or thenable value.
thenable
An object or function with a then method.
JavaScript engines use duck-typing for promises.
Arrays, functions, and objects with a then method will be treated as promise-like objects, and work with built-in
mechanisms like Promise.resolve and the await keyword like native promises.
Some thenables also implement a catch method (like native promises).
When available, when uses it to ensure rejections are handled.
Project
Version
when adheres to semver.
Contribute
See CONTRIBUTING.md.
This project has a code of conduct. By interacting with this repository, organization, or community you agree to abide by its terms.
Sponsor
This package is intentionally small — and intentionally maintained.
Small primitives power larger systems. Support long-term stability by sponsoring Flex Development.
