Go

What is "asserting errors for behavior"? How would you implement retry logic based on error behavior?

Question 322MediumGo 1.22 to 1.25

Instead of checking for a concrete type or sentinel, check whether the error implements a small interface that describes a behavior. The producer can then change its concrete types freely, and callers don't need to import them. The standard example is net.Error with Timeout() bool. Its Temporary() method is deprecated because "temporary" was poorly defined.

type retryable interface{ Retryable() bool }

type RateLimitErr struct{ RetryAfter time.Duration }

func (e *RateLimitErr) Error() string   { return "rate limited" }
func (e *RateLimitErr) Retryable() bool { return true }

func shouldRetry(err error) bool {
	var r retryable
	if errors.As(err, &r) && r.Retryable() {
		return true
	}
	var ne net.Error
	return errors.As(err, &ne) && ne.Timeout()
}

func Do(ctx context.Context, op func() error) error {
	var err error
	for attempt := range 5 {
		if err = op(); err == nil || !shouldRetry(err) {
			return err
		}
		select {
		case <-time.After(time.Duration(1<<attempt) * 100 * time.Millisecond):
		case <-ctx.Done():
			return errors.Join(err, context.Cause(ctx))
		}
	}
	return fmt.Errorf("gave up after retries: %w", err)
}

Use errors.As with an interface target so wrapped errors are found. A direct type assertion misses them.

More on Error Handling & panics

All 37 Error Handling & panics questions