Execute a callback that produces either a Task or the “sentinel”
Error subclass StopRetrying. withRetries retries
the retryable callback until the retry strategy is exhausted or until the
callback returns either StopRetrying or a Task that rejects with
StopRetrying. If no strategy is supplied, a default strategy of retrying
immediately up to three times is used.
The strategy is any iterable iterator that produces an integral number,
which is used as the number of milliseconds to delay before retrying the
retryable. When the strategy stops yielding values, this will produce a
RejectedTask whose rejection value is an instance of
RetryFailed.
Returning stopRetrying() from the top-level of the function or as the
rejection reason will also produce a rejected Task whose rejection value is
an instance of RetryFailed, but will also immediately stop all further
retries and will include the StopRetrying instance as the cause of the
RetryFailed instance.
You can determine whether retries stopped because the strategy was exhausted
or because stopRetrying was called by checking the cause on the
RetryFailed instance. It will be undefined if the the RetryFailed was
the result of the strategy being exhausted. It will be a StopRetrying if it
stopped because the caller returned stopRetrying().
Examples
Retrying with backoff
When attempting to fetch data from a server, you might want to retry if and
only if the response was an HTTP 408 response, indicating that there was a
timeout but that the client is allowed to try again. For other error codes, it
will simply reject immediately.
Sometimes, you may determine that the result of an operation is fatal, so
there is no point in retrying even if the retry strategy still allows it. In
that case, you can return the special StopRetrying error produced by calling
stopRetrying to immediately stop all further retries.
For example, imagine you have a library function that returns a custom Error
subclass that includes an isFatal value on it, something like this::
Every time withRetries tries the retryable, it provides the current count
of attempts and the total elapsed duration as properties on the status
object, so you can do different things for a given way of trying the async
operation represented by the Task depending on the count. Here, for example,
the task is retried if the HTTP request rejects, with an exponential backoff
starting at 100 milliseconds, and captures the number of retries in an Error
wrapping the rejection reason when the response rejects or when converting the
response to JSON fails. It also stops if it has tried the call more than 10
times or if the total elapsed time exceeds 10 seconds.
While the task/delay module supplies a number of useful strategies,
you can also supply your own. The easiest way is to write [a generator
function][gen], but you can also implement a custom iterable iterator,
including by providing a subclass of the ES2025 Iterator class.
Here is an example of using a generator function to produce a random but
monotonically increasing value proportional to the current
value:
import*asTaskfrom'true-myth/task';
function*randomIncrease(options?: { from: number }) { // always use integral values, and default to one second. letvalue = options ? Math.round(options.from) : 1_000; while (true) { yieldvalue; value += Math.ceil(Math.random() * value); // always increase! } }
An iterable iterator that produces an integral number of
milliseconds to wait before trying retryable again. If not supplied, the
retryable will be retried immediately up to three times.
Execute a callback that produces either a
Task
or the “sentinel”Error
subclassStopRetrying
.withRetries
retries theretryable
callback until the retry strategy is exhausted or until the callback returns eitherStopRetrying
or aTask
that rejects withStopRetrying
. If no strategy is supplied, a default strategy of retrying immediately up to three times is used.The
strategy
is any iterable iterator that produces an integral number, which is used as the number of milliseconds to delay before retrying theretryable
. When thestrategy
stops yielding values, this will produce aRejected
Task
whose rejection value is an instance ofRetryFailed
.Returning
stopRetrying()
from the top-level of the function or as the rejection reason will also produce a rejectedTask
whose rejection value is an instance ofRetryFailed
, but will also immediately stop all further retries and will include theStopRetrying
instance as thecause
of theRetryFailed
instance.You can determine whether retries stopped because the strategy was exhausted or because
stopRetrying
was called by checking thecause
on theRetryFailed
instance. It will beundefined
if the theRetryFailed
was the result of the strategy being exhausted. It will be aStopRetrying
if it stopped because the caller returnedstopRetrying()
.Examples
Retrying with backoff
When attempting to fetch data from a server, you might want to retry if and only if the response was an HTTP 408 response, indicating that there was a timeout but that the client is allowed to try again. For other error codes, it will simply reject immediately.
Here, this uses a Fibonacci backoff strategy, which can be preferable in some cases to a classic exponential backoff strategy (see A Performance Comparison of Different Backoff Algorithms under Different Rebroadcast Probabilities for MANET's for more details).
Manually canceling retries
Sometimes, you may determine that the result of an operation is fatal, so there is no point in retrying even if the retry strategy still allows it. In that case, you can return the special
StopRetrying
error produced by callingstopRetrying
to immediately stop all further retries.For example, imagine you have a library function that returns a custom
Error
subclass that includes anisFatal
value on it, something like this::You could check that flag in a
Task
rejection and returnstopRetrying()
if it is set:Using the retry
status
parameterEvery time
withRetries
tries theretryable
, it provides the current count of attempts and the total elapsed duration as properties on thestatus
object, so you can do different things for a given way of trying the async operation represented by theTask
depending on the count. Here, for example, the task is retried if the HTTP request rejects, with an exponential backoff starting at 100 milliseconds, and captures the number of retries in anError
wrapping the rejection reason when the response rejects or when converting the response to JSON fails. It also stops if it has tried the call more than 10 times or if the total elapsed time exceeds 10 seconds.Custom strategies
While the task/delay module supplies a number of useful strategies, you can also supply your own. The easiest way is to write [a generator function][gen], but you can also implement a custom iterable iterator, including by providing a subclass of the ES2025
Iterator
class.Here is an example of using a generator function to produce a random but monotonically increasing value proportional to the current value: