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
- Q320Explain context errors:
context.Canceled,DeadlineExceeded, andcontext.Cause. - Q321Why is
errors.Is(err, fs.ErrNotExist)preferred overos.IsNotExist(err)? - Q323How does
net/httphandle a panic in a handler? What are the pitfalls? - Q324How would you design an error model for a large service, for example with Op, Kind and a wrapped cause?
- Q325What is the
io.Readererror contract, and why isif err != nil { return }wrong there? - Q326Implement a wrapper error type that adds context and works with
errors.Is/As. What happens if you forgetUnwrap?