DevPill 10 - Fault tolerance: adding retries to your Go code

Published: (December 11, 2025 at 08:21 PM EST)
2 min read
Source: Dev.to

Source: Dev.to

Adding retries to your API

Adding retries to your API is a must to make your system more resilient. You can add them in database operations, communication with external APIs, and any other operation that might depend on a response from a third party.

1. Retry function

The function receives the number of attempts, a delay (interval between attempts), and the function that represents the operation you want to perform.

func Retry(attempts int, delay time.Duration, fn func() error) error {
    err := fn()
    if err == nil {
        return nil
    }

    if !IsTransientError(err) {
        // log.Println("--- retry not needed")
        return err
    }

    if attempts--; attempts > 0 {
        time.Sleep(delay)
        // log.Println("--- trying again...")
        return Retry(attempts, delay*2, fn) // exponential backoff
    }

    return err
}

2. IsTransientError function

This helper checks whether an error is transient and worth retrying. For example, sql.ErrNoRows indicates no rows were found and should not be retried.

func IsTransientError(err error) bool {
    if err == nil {
        return false
    }

    // example: deadlock postgres
    if strings.Contains(err.Error(), "deadlock detected") {
        return true
    }

    // closed connection
    if strings.Contains(err.Error(), "connection reset by peer") {
        return true
    }

    // network/postgres timeout
    if strings.Contains(err.Error(), "timeout") {
        return true
    }

    // max connections exceeded
    if strings.Contains(err.Error(), "too many connections") {
        return true
    }

    return false
}

3. Using the function

Here’s an example of retrying a database operation in a login method within the service layer:

func (s *UserService) Login(ctx context.Context, email, password string) (*UserOutput, error) {
    var user *domain.User
    err := retry.Retry(3, 200*time.Millisecond, func() error {
        var repoError error
        user, repoError = s.repo.GetUserByEmail(ctx, email)
        if repoError != nil {
            if repoError == sql.ErrNoRows {
                return ErrUserNotFound
            }
            return repoError
        }
        return nil
    })

    if err != nil {
        return nil, err
    }
    // ... further processing ...
    return &UserOutput{/* fields */}, nil
}
Back to Blog

Related posts

Read more »