Basic usage
Constructor#
You create a LazyPromise like you create a native promise, except you have a sink object instead of a resolve, reject pair, and you can optionally return a teardown function:
const lazyPromise = new LazyPromise<number>((sink) => {
const timeoutId = setTimeout(() => {
if (...) {
sink.resolve(42);
} else {
sink.reject(new Error("oops"));
}
}, 1000);
return () => {
clearTimeout(timeoutId);
};
});Subscribing#
A LazyPromise doesn’t do anything until you subscribe to it:
const subscription = lazyPromise.subscribe({
resolve: (value) => ...,
reject: (error) => ...,
});To cancel the subscription, you call
// This method is idempotent.
subscription.dispose();Invariants#
Whereas a native promise executes eagerly and once, a LazyPromise behaves like an Observable, that is it runs the constructor callback each time someone subscribes. The way to think of it is if foo is a function like
<Value>(arg: {
resolve: (value: Value) => void;
reject: (error: unknown) => void;
}) => {
...
return () => {
...
};
};then new LazyPromise(foo) is simply foo with a wrapper around it that’s only there to enforce a few invariants:
-
If something gets emitted, that only happens once.
-
Nothing gets emitted after you unsubscribe.
-
The teardown logic is run at most once.
-
The teardown logic is run before something gets emitted.
-
There can be no higher-order LazyPromise (a LazyPromise that resolves to a LazyPromise). If you call the
resolvehandle of a nativePromisewith aPromise<string>as an argument, you’ll end up withPromise<string>, notPromise<Promise<string>>, so it’s physically impossible to create a higher-order Promise. LazyPromise is similarly flattened.
Microtasks#
Also like Observable, LazyPromise does not defer notifications to microtasks. A native promise guarantees that in promise.then(foo); bar();, foo runs after bar, but that guarantee comes at a cost: if for example you have two async functions that each await a few resolved promises, which of them will finish last will depend on which one has more awaits in it. (It’s jumping a bit ahead, but if you want a LazyPromise to fire in a microtask, add .finally(inMicrotask).)
Promise-like API#
Aside from superficial differences, the LazyPromise API mirrors that of the native promise:
| Promise API | LazyPromise equivalent |
|---|---|
promise.then(foo) |
lazyPromise.map(foo) |
promise.catch(foo) |
lazyPromise.catch(foo) |
promise.finally(foo) |
lazyPromise.finally(foo) |
Promise.resolve(valueOrPromise) |
box(valueOrLazyPromise) |
Promise.reject(error) |
rejecting(error) |
new Promise<never>(() => {}) |
never |
Promise.all(...) |
all(...) |
Promise.race(...) |
race(...) |
Awaited<T> |
Unbox<T> |
(any, allSettled and withResolvers are discussed separately in these docs.)
Canceling a LazyPromise automatically cancels any upstream LazyPromise it was derived from via the operators above.
pipe#
LazyPromise has a method pipe that allows you to dot-chain custom operators or other functions that take a LazyPromise as a single parameter: lazyPromise.pipe(foo) is equivalent to foo(lazyPromise).