purrr insistently() in R: Retry a Call Until It Succeeds
purrr insistently() wraps a function so it retries automatically after an error instead of failing. The wrapped function waits a short pause, calls the original code again, and keeps trying until it succeeds or the retry budget runs out.
insistently(f) # retry with default backoff insistently(f, rate_backoff(max_times = 5)) # allow up to 5 attempts insistently(f, rate_delay(pause = 2)) # fixed 2s gap between tries insistently(f, quiet = FALSE) # print each error and retry insistently(f)() # build the wrapper, then call it rate_backoff(pause_base = 1, pause_cap = 60) # tune exponential backoff map(urls, insistently(fetch)) # retry inside a map
Need explanation? Read on for examples and pitfalls.
What purrr insistently() does
insistently() turns a fragile function into a stubborn one. It is a purrr adverb: you hand it a function and it returns a new function that does the same job, except it retries whenever the original raises an error. Each retry waits a short pause first. The loop ends when a call finally succeeds, or when the retry budget is exhausted and the last error is raised.
This is built for transient failures. A network request times out, an API returns a rate-limit error, a file is briefly locked. None of these are real bugs, and a second attempt a moment later usually works. insistently() automates that "just try again" reflex.
The catch is that insistently() never judges why a call failed. It retries on any error, transient or permanent. That makes it a precise fit for unreliable infrastructure and a poor fit for logic bugs.
Syntax and arguments
insistently() takes a function, a rate, and a quiet flag. Only the first argument is required.
| Argument | Default | What it controls |
|---|---|---|
f |
(required) | The function to make retry-capable. |
rate |
rate_backoff() |
A rate object setting pause length and max attempts. |
quiet |
TRUE |
When FALSE, prints each error and the next pause. |
The rate argument is where the real tuning happens. Two helpers build it. rate_backoff() uses exponential back-off, so attempt i waits roughly pause_base * 2^i seconds, capped at pause_cap. rate_delay() uses a constant pause between every attempt. Both accept max_times, the hard ceiling on attempts.
rate_backoff() also accepts jitter, on by default, which adds randomness to every pause. Jitter matters when many processes retry at once: without it they all wake together and hammer the recovering service in lockstep.
insistently(f) once to build the retrying wrapper, then call that wrapper to actually run the work.Retry a flaky function
Start with a function that fails before it succeeds. The example below fails its first two calls, then works, which mimics a connection that needs a couple of tries. The global attempt counter lets the function behave differently on each call, so the retry logic has something real to react to.
Wrapping it with insistently() produces a function that absorbs those early failures. Here rate_backoff() allows up to five attempts with tiny pauses so the example runs quickly.
The first two errors never surface to the caller. With exponential back-off each wait is roughly double the last, giving a struggling service more room to recover than a fixed gap would.
To retry on a fixed interval instead of growing pauses, swap in rate_delay(). This suits polling, where you check a job every few seconds until it is ready. A constant interval is the right choice when the remote system defines the cadence, such as an API that asks you to poll a status endpoint at a steady rate.
By default quiet = TRUE hides the failures, which keeps production logs clean since a call that eventually succeeds is not a problem. Set quiet = FALSE while developing to watch each retry and pause as it happens.
rate_backoff() stops after three attempts. For a genuinely flaky service, raise max_times so a brief outage does not exhaust the budget before recovery.Use insistently() inside map()
insistently() composes cleanly with the map family. Because it returns a plain function, you can drop it straight into map() and every element gets its own retry loop.
Each of the three iterations retries until the counter lands on a multiple of three. One slow or flaky element no longer aborts the whole iteration. This is the everyday pattern for batch work against a network: wrap the fetch with insistently() and each ID retries on its own.
insistently() vs the other purrr adverbs
insistently() is one of five purrr adverbs for handling failure. Pick the one that matches your intent: retry, capture, fall back, or pace.
| Adverb | On error | Returns | Use when |
|---|---|---|---|
insistently() |
retries f |
result of f, or final error |
failures are transient |
safely() |
captures error | list of result and error |
you want to inspect every failure |
possibly() |
swallows error | a default otherwise value |
a failure should not stop a pipeline |
slowly() |
does not handle errors | result of f |
you must pace calls, not retry them |
The decision rule is simple. Use insistently() when a second attempt is likely to work. Use possibly() or safely() when an error is final and you only need to handle it gracefully. Reach for slowly() when the goal is rate limiting rather than recovery.
Common pitfalls
Three mistakes account for most insistently() trouble. All are easy to avoid once you know them.
First, calling insistently(f) and expecting a value. It returns a function, so you must call the result. insistently(f)() runs once; assigning the wrapper to a name is clearer.
Second, retrying side effects. If f writes a row or sends an email, every retry repeats that action. The code below appends to a log on each failed attempt, so a function that succeeds on attempt three writes three lines.
Third, expecting insistently() to tell a transient error from a real bug. It cannot. It retries on any error, so a typo in f will simply be retried until the budget runs out.
Try it yourself
Try it: Build a function ex_unstable that fails its first two calls and then returns "ok". Wrap it with insistently() allowing up to four attempts, call the wrapper, and save the result to ex_result.
Click to reveal solution
Explanation: insistently() builds ex_robust from ex_unstable. The first two calls error, the third returns "ok", and the wrapper hands back that success because four attempts were allowed.
Related purrr functions
These adverbs and helpers pair naturally with insistently():
safely()captures each error as a value instead of retrying.possibly()returns a fixed fallback when a call fails.quietly()captures warnings and messages alongside the result.rate_backoff()andrate_delay()build therateobject that controls pacing.map()applies a retrying function across a list or vector.
See the official purrr insistently() reference for the full rate-helper documentation.
FAQ
What does insistently() do in R?
insistently() takes a function and returns a modified version that retries after an error. The new function calls the original code, and if it fails, waits a short pause and tries again. Retrying continues until a call succeeds or the maximum attempts are used up. It is purrr's tool for handling transient failures such as network timeouts without writing manual retry loops.
How many times does insistently() retry?
The retry count comes from the rate object, not from insistently() itself. The default rate_backoff() allows three attempts. Pass rate_backoff(max_times = 5) or rate_delay(max_times = 10) to change the ceiling. Once the limit is reached, insistently() stops retrying and raises the most recent error so the failure is not hidden.
What is the difference between insistently() and slowly()?
Both adverbs add pauses, but for different reasons. insistently() retries a function after it errors, so the pause happens between failed attempts. slowly() does not handle errors at all; it simply inserts a delay before each call so you do not exceed a rate limit. Use insistently() for recovery and slowly() for pacing.
Does insistently() work with map()?
Yes. Because insistently() returns an ordinary function, you can pass it straight to map(), map_dbl(), or any iteration helper. Each element then gets its own independent retry loop, so a single flaky input no longer aborts the whole iteration. This is the standard pattern for retrying a request across a list of URLs or IDs.
Can insistently() retry forever?
You can set rate_delay(max_times = Inf) to retry indefinitely, but that risks an infinite loop if the function never succeeds. A finite ceiling is safer: it guarantees the call eventually returns control and surfaces the underlying error rather than hanging.