retry

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package retry retries a function with exponential backoff. It is a stdlib-only Go port of the npm "retry" package (https://www.npmjs.com/package/retry, also published as node-retry) and the higher-level "p-retry" / "async-retry" wrappers built on top of it. Those JavaScript libraries repeatedly invoke an operation that may fail transiently — a flaky network request, a database call, a rate-limited API — pausing between attempts for a delay that grows on each failure, and they are the retry engine behind a large part of the Node ecosystem. This package brings the same retry-with-backoff contract to Go using only the standard library.

The central entry point is Do, which calls fn and, on error, sleeps and calls it again until the operation succeeds, the retry budget is exhausted, or an AbortError signals that further attempts are pointless. fn receives the 1-based attempt number so it can log or vary behavior across attempts. When every attempt fails, the error from the final attempt is returned unchanged; when an AbortError is returned, its wrapped error (if any) is returned and no further attempts are made. This mirrors p-retry, where returning an AbortError short-circuits the retry loop.

Backoff follows the node-retry formula: the delay before retry attempt n (1-based, where attempt 1 is the pause before the first retry) is MinTimeout * Factor^(n-1), capped at MaxTimeout. Factor defaults to 2, MinTimeout defaults to one second, and MaxTimeout defaults to effectively no cap. With the defaults this yields the classic doubling schedule 1s, 2s, 4s, 8s, and so on; a Factor of 10 with a MinTimeout of 10ms and MaxTimeout of 100ms yields 10ms, 100ms, 100ms, 100ms as the exponential growth saturates at the cap. The Retries option counts retries after the first attempt, so Retries of 3 means up to four total invocations, and Retries of 0 means fn is attempted exactly once.

One intentional deviation from node-retry is that this port does not apply randomized jitter. node-retry exposes a "randomize" option that multiplies each delay by a random factor between 1 and 2 to spread out retries from many clients (the thundering-herd problem); here the backoff is fully deterministic, which keeps the schedule predictable and testable. Callers who need jitter can wrap Do or add it inside a custom Sleep. The Backoff method is exported so the exact schedule can be inspected or reused without running the retry loop.

Timing is injectable: Options.Sleep replaces the default time.Sleep, and Options.OnRetry is invoked before each retry with the failing error and the attempt number that just failed. Together these make the backoff schedule observable and testable without waiting on real time, and let callers surface retry activity to logs or metrics. Do is safe to call concurrently as long as the supplied fn and callbacks are themselves safe; the package holds no shared state between calls.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Do

func Do(fn func(attempt int) error, opts Options) error

Do calls fn, retrying with exponential backoff on error until it succeeds, the retry budget is exhausted, or an AbortError is returned.

fn receives the 1-based attempt number. The error returned by the final failed attempt is returned to the caller (unwrapped from AbortError).

Example

ExampleDo retries a failing operation until it succeeds. The supplied function receives the 1-based attempt number and fails on the first two attempts, succeeding on the third. To keep the example fast and deterministic the Sleep hook is replaced with a no-op so no real time passes between attempts. Do returns nil once the operation succeeds, and here the function was invoked exactly three times. Retries of 5 means up to six total invocations were permitted.

package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/malcolmston/express/retry"
)

func main() {
	count := 0
	err := retry.Do(func(attempt int) error {
		count++
		if attempt < 3 {
			return errors.New("transient failure")
		}
		return nil
	}, retry.Options{Retries: 5, Sleep: func(time.Duration) {}})
	fmt.Println(count, err)
}
Output:
3 <nil>

Types

type AbortError

type AbortError struct {
	Err error
}

AbortError wraps an error to signal that retrying must stop immediately. The wrapped error is returned to the caller of Do.

func Abort

func Abort(err error) *AbortError

Abort creates an AbortError that stops retrying immediately when returned from the retried function.

Example

ExampleAbort stops retrying immediately by returning an AbortError, even though the retry budget has not been exhausted. The wrapped error is returned to the caller unchanged and no further attempts are made, which is the right behavior for a permanent failure such as an authentication error where retrying is pointless. Here the function is called only once. Abort mirrors p-retry's AbortError, which short-circuits the retry loop.

package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/malcolmston/express/retry"
)

func main() {
	count := 0
	err := retry.Do(func(attempt int) error {
		count++
		return retry.Abort(errors.New("permanent failure"))
	}, retry.Options{Retries: 5, Sleep: func(time.Duration) {}})
	fmt.Println(count, err)
}
Output:
1 permanent failure

func (*AbortError) Error

func (a *AbortError) Error() string

Error implements the error interface.

func (*AbortError) Unwrap

func (a *AbortError) Unwrap() error

Unwrap returns the wrapped error so errors.Is / errors.As work through it.

type Options

type Options struct {
	// Retries is the maximum number of retries after the first attempt. A
	// value of 0 means the function is attempted exactly once. This mirrors
	// the "retries" option of node-retry / p-retry.
	Retries int
	// Factor is the exponential backoff factor. Defaults to 2 when zero.
	Factor float64
	// MinTimeout is the delay before the first retry. Defaults to 1s when
	// zero.
	MinTimeout time.Duration
	// MaxTimeout caps the delay between retries. Defaults to no cap
	// (math.MaxInt64) when zero.
	MaxTimeout time.Duration
	// OnRetry, if set, is called before each retry with the error that caused
	// the retry and the attempt number that just failed (1-based).
	OnRetry func(err error, attempt int)
	// Sleep, if set, is used instead of time.Sleep. It makes the backoff
	// injectable for testing.
	Sleep func(d time.Duration)
}

Options configures Do.

func (Options) Backoff

func (o Options) Backoff(attempt int) time.Duration

Backoff computes the delay before the given retry attempt. attempt is 1-based, where attempt 1 is the delay before the first retry.

The formula is min(MaxTimeout, MinTimeout * Factor^(attempt-1)).

Example

ExampleOptions_Backoff inspects the backoff schedule without running the retry loop. With a Factor of 2 and a MinTimeout of one second the delays double on each attempt: 1s, 2s, then 4s. The MaxTimeout of 5 seconds caps the fourth delay, which would otherwise be 8 seconds, at 5 seconds. The attempt argument is 1-based, where attempt 1 is the pause before the first retry. This lets callers verify or reuse the exact schedule the retry loop will follow.

package main

import (
	"fmt"
	"time"

	"github.com/malcolmston/express/retry"
)

func main() {
	o := retry.Options{Factor: 2, MinTimeout: time.Second, MaxTimeout: 5 * time.Second}
	fmt.Println(o.Backoff(1), o.Backoff(2), o.Backoff(3), o.Backoff(4))
}
Output:
1s 2s 4s 5s

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL