bask-promise
v1.1.2
Published
Set of common promise utilities, not available in basic node.js features * Delay promise * Delay before or after promise * Wait until a minimum time has elapsed * Delay a rejection * Run promises in sequence, e.g. requests to database * Repeated fai
Readme
BASK Promise
Set of common promise utilities, not available in basic node.js features
- Delay promise
- Delay before or after promise
- Wait until a minimum time has elapsed
- Delay a rejection
- Run promises in sequence, e.g. requests to database
- Repeated failed promises
- Repeated failed promises with exponential backoff
- Timeout a promise
- Race a promise against a guard promise
- Deferred promise with state inspection
Install
npm i bask-promise --saveThen import module in code:
import * as promises from 'bask-promise';
promises.delay(100);Or
import { delay } from 'bask-promise';
delay(100);Delay
Simple delay
import { delay } from 'bask-promise';
delay(1000).then(() => console.log('Run after 1 second'));Delay after promise
import { delayAfter } from 'bask-promise';
delayAfter(Promise.resolve('1 second delay after'), 1000);Delay before promise
import { delayBefore } from 'bask-promise';
delayBefore(() => Promise.resolve('1 second delay before'), 1000);Random delay
Random time period delay
import { random } from 'bask-promise';
random(1000).then(() => console.log('Run after 0...1 second'));Specify lower bound
import { random } from 'bask-promise';
random(2000, 1000).then(() => console.log('Run after 1...2 seconds'));Random delay after promise
import { randomAfter } from 'bask-promise';
randomAfter(Promise.resolve('0...1 second delay after'), 1000);
randomAfter(Promise.resolve('0...2 second delay after'), 2000, 1000);Random delay before promise
import { randomBefore } from 'bask-promise';
randomBefore(() => Promise.resolve('0...1 second delay before'), 1000);
randomBefore(() => Promise.resolve('1...2 second delay before'), 2000, 1000);Delay callback
If you want to use delay as a callback inside then, you can use delayFun(ms)
import { delayFun } from 'bask-promise';
Promise.resolve("Delay for 1s in then after promise").then(delayFun(1000));Delay till
Waits until both the promise resolves and the minimum time has elapsed. Useful for avoiding UI flicker on fast operations — e.g. keeping a spinner visible for at least 500ms.
import { delayTill } from 'bask-promise';
// Resolves after at least 5000ms, even if fetchData() finishes in 1000ms
delayTill(fetchData(), 5000);If the promise takes longer than milliseconds, it resolves as soon as the promise does:
// fetchData() takes 6000ms, milliseconds is 5000ms → resolves after 6000ms
delayTill(fetchData(), 5000);By default, errors are propagated immediately. Set delayError: true to also delay rejection:
// If fetchData() fails early, the rejection is still held until 5000ms have passed
delayTill(fetchData(), 5000, true);Delay throw
Returns a .catch-compatible callback that delays a rejection by the given number of milliseconds before re-throwing. Useful as a building block or as an inline callback.
import { delayThrow } from 'bask-promise';
// Delays the rejection by 1 second before propagating it
fetchData().catch(delayThrow(1000));Combined with delayTill:
import { delayTill, delayThrow } from 'bask-promise';
// Equivalent to delayTill(fetchData(), 5000, true)
delayTill(fetchData().catch(delayThrow(5000)), 5000);Sequence
Node.js have basic Promise.all function to run promises in parallel, but to run sequence you have to implement it yourself. This is a typical task for example when quering database or external services.
import { sequence } from 'bask-promise';
sequence([
() => Promise.resolve('Query database 1'),
() => Promise.resolve('Query database 2'),
])If you have the same query function, but different arguments - use keySequence:
import { keySequence } from 'bask-promise';
keySequence([1,2,3,4,5], key => queryDatabasePromise(key));If you want to mix sequence and parallel mode and define concurrency number - use concurrent. For concurrent <= 1 - this is sequence() For concurrent >= array length - this is Promise.all()
import { concurrent } from 'bask-promise';
concurrent(2, [
() => Promise.resolve('Query database 1 (first thread)'),
() => Promise.resolve('Query database 2 (second thread)'),
() => Promise.resolve('Query database 3 (first thread)'),
() => Promise.resolve('Query database 4 (second thread)')
])Repeat
Sometimes you need to ensure request that is properly executed. But due to network connections, requests can fail. In that case you may want to repeat failed requests automatically.
Repeat up to 3 times (4 calls total):
import { repeat } from 'bask-promise';
repeat(() => repeatThisPromiseIfFails(), 3);Repeat until success:
repeat(() => repeatThisPromiseIfFails(), -1);Log every error
repeat(() => repeatThisPromiseIfFails(), 3, error => console.error(error));Delay after error
import { repeat, delay } from 'bask-promise';
repeat(() => repeatThisPromiseIfFails(), 3, error => delay(1000));Options object variant
All parameters can be passed as a single RepeatOptions object instead. This is the recommended style when specifying multiple options together, as it is more readable and self-documenting.
interface RepeatOptions<T> {
promiseFun: () => Promise<T>;
times?: number; // default: 1
onError?: (error: Error) => void;
shouldRetry?: (error: Error, attempt: number) => boolean;
backoff?: (attempt: number) => number;
jitter?: 'full' | 'equal' | false; // default: false
signal?: AbortSignal;
}Repeat up to 3 times:
import { repeat } from 'bask-promise';
repeat({ promiseFun: () => repeatThisPromiseIfFails(), times: 3 });Repeat until success:
repeat({ promiseFun: () => repeatThisPromiseIfFails(), times: -1 });Log every error and delay after each failure:
import { repeat, delay } from 'bask-promise';
repeat({
promiseFun: () => repeatThisPromiseIfFails(),
times: 3,
onError: error => { console.error(error); return delay(1000); },
});Retry only on specific errors:
import { repeat } from 'bask-promise';
// Only retry on network errors, fail immediately on anything else
repeat({
promiseFun: () => fetchData(),
times: 3,
shouldRetry: error => error instanceof NetworkError,
});Retry based on error code:
repeat({
promiseFun: () => fetchData(),
times: 5,
shouldRetry: err => err.code === 'ECONNRESET',
});Use the attempt argument to limit retries by error type and count together:
repeat({
promiseFun: () => fetchData(),
times: 10,
shouldRetry: (error, attempt) => error instanceof NetworkError && attempt < 3,
});Use backoff to add a delay before each retry. The function receives the current attempt number (starting at 0) and returns a delay in milliseconds:
Fixed delay between retries:
repeat({
promiseFun: () => fetchData(),
times: 5,
backoff: () => 1000,
});Exponential backoff (doubles each attempt, capped at 30 seconds):
import { repeat, exponential } from 'bask-promise';
repeat({
promiseFun: () => fetchData(),
times: 10,
backoff: exponential(), // 1000ms, 2000ms, 4000ms … capped at 30000ms
});exponential(baseDelay?, maxDelay?) is a convenience factory for the common pattern attempt => Math.min(baseDelay * 2 ** attempt, maxDelay). Both parameters are optional:
| Parameter | Default | Description |
|---|---|---|
| baseDelay | 1000 | Delay for the first retry in milliseconds |
| maxDelay | 30000 | Upper cap in milliseconds |
exponential() // 1000ms → 2000ms → 4000ms … → 30000ms
exponential(500) // 500ms → 1000ms → 2000ms … → 30000ms
exponential(200, 5000) // 200ms → 400ms → 800ms … → 5000msCombine backoff with shouldRetry and onError:
import { repeat, exponential } from 'bask-promise';
repeat({
promiseFun: () => fetchData(),
times: 5,
shouldRetry: error => error instanceof NetworkError,
backoff: exponential(1000, 30_000),
onError: error => console.error(`Attempt failed: ${error.message}`),
});Use jitter to add randomness to backoff delays, which prevents many clients from retrying in sync and hammering the server at the same time (AWS Architecture Blog).
| Value | Behaviour |
|---|---|
| false | No jitter — exact backoff value is used (default) |
| 'full' | Uniformly random between 0 and the full backoff: random(0, backoff) |
| 'equal' | Keeps half the backoff, randomises the other half: backoff/2 + random(0, backoff/2) |
'full' jitter — most effective at spreading load:
import { repeat, exponential } from 'bask-promise';
repeat({
promiseFun: () => fetchData(),
times: 10,
backoff: exponential(),
jitter: 'full',
});'equal' jitter — steadier slowdown with some spread:
repeat({
promiseFun: () => fetchData(),
times: 10,
backoff: exponential(),
jitter: 'equal',
});jitter without backoff has no effect, since there is no delay to randomise.
Pass an AbortSignal via signal to cancel the retry loop at any point. The returned promise rejects with the abort reason as soon as the signal fires — either immediately if the signal is already aborted, or mid-backoff-delay if it fires while waiting between retries.
Cancel after a timeout using AbortSignal.timeout:
import { repeat, exponential } from 'bask-promise';
repeat({
promiseFun: () => fetchData(),
times: -1,
backoff: exponential(),
signal: AbortSignal.timeout(10_000), // give up entirely after 10 seconds
});Cancel manually with AbortController:
import { repeat } from 'bask-promise';
const ac = new AbortController();
repeat({
promiseFun: () => fetchData(),
times: -1,
backoff: () => 1000,
signal: ac.signal,
});
// cancel from outside whenever needed
ac.abort(new Error('user cancelled'));Repeat with exponential backoff
Like repeat, but automatically adds an increasing delay between retries. The delay doubles after each failed attempt: baseDelay * 2^attempt.
Repeat up to 3 times with default 100ms base delay (delays: 100ms, 200ms, 400ms):
import { repeatExponential } from 'bask-promise';
repeatExponential({ promiseFun: () => repeatThisIfFails(), times: 3 });Custom base delay:
repeatExponential({ promiseFun: () => repeatThisIfFails(), times: 5, baseDelay: 200 });Repeat until success:
repeatExponential({ promiseFun: () => repeatThisIfFails(), times: -1 });Log every error:
repeatExponential({
promiseFun: () => repeatThisIfFails(),
times: 3,
onError: error => console.error(error),
});Promise resolve from outside
If you need to create a promise instance, pass it somewhere, and resolve it later from outside,
you can use deferred function
import { deferred } from 'bask-promise';
const d = deferred();
// use or pass promise somewhere
d.promise.then(() => null).catch(() => null);
// then resolve or reject from outside
d.resolve(42);
// or
d.reject(new Error('foo'));Inspecting deferred state
The deferred object exposes read-only properties to inspect its current state at any time:
| Property | Type | Description |
|---|---|---|
| isPending | boolean | true until resolved or rejected |
| isResolved | boolean | true after resolve() is called |
| isFailed | boolean | true after reject() is called |
| value | T \| undefined | The resolved value (available after the next microtask), undefined otherwise |
| error | unknown \| undefined | The rejection reason, undefined otherwise |
import { deferred } from 'bask-promise';
const d = deferred();
d.isPending; // true
d.isResolved; // false
d.isFailed; // false
d.value; // undefined
d.error; // undefined
d.resolve(42);
d.isPending; // false
d.isResolved; // true
await Promise.resolve(); // wait one microtask tick
d.value; // 42const d = deferred();
d.reject(new Error('boom'));
d.isPending; // false
d.isFailed; // true
d.error; // Error: boomTimer
Creates a promise that rejects after a given delay. Useful as a standalone building block or combined with left.
import { timer } from 'bask-promise';
// Rejects with default message after 1 second
timer(1000);
// Rejects with a custom message
timer(1000, 'Operation took too long');Timeout
Races a promise against a timer. Rejects with a timeout error if the promise does not settle within the given milliseconds.
import { timeout } from 'bask-promise';
// Rejects with "Timeout exceeded: 5000ms" if fetchData() takes longer than 5 seconds
timeout(fetchData(), 5000);Left
Races a promise against a guard promise. Resolves with the value of the primary promise, but rejects if the guard promise rejects — regardless of the primary promise state. The resolved value of the guard is ignored.
import { left } from 'bask-promise';
// Resolves with the result of primary(), but aborts if guard rejects
left(primary(), guard());A practical use case is combining left with timer to build a custom timeout with a specific error message:
import { left, timer } from 'bask-promise';
left(fetchData(), timer(5000, 'fetchData timed out'));