---
title: "Errors and retries"
description: "One error type for every failure, a sentinel for every kind, and retries that cannot send twice."
url: "https://openemail.uk/docs/go/errors"
area: "Go"
category: "Getting started"
---

# Errors and retries

One error type for every failure, a sentinel for every kind, and retries that cannot send twice.

## Handling an error

**errors.go**

```
_, err := client.Emails.Send(ctx, openemail.Body{
	"from":    "billing@acme.com",
	"to":      "ada@example.com",
	"subject": "Your September invoice",
	"text":    "Invoice attached.",
})

var failure *openemail.Error

switch {
case err == nil:
	fmt.Println("sent")
case errors.Is(err, openemail.ErrValidation):
	errors.As(err, &failure)
	fmt.Println(failure.Code, failure.Param, failure.Message)
case errors.Is(err, openemail.ErrRateLimited):
	errors.As(err, &failure)
	fmt.Println("try again in", failure.RetryAfter)
case errors.Is(err, openemail.ErrTimeout):
	fmt.Println("no answer in time")
default:
	return err
}
```

Every failure is an `*openemail.Error`. `errors.Is` tells which kind it is, and `errors.As` gives the error itself, with what the API said about it.

| Field | What it holds |
| --- | --- |
| `Status` | The HTTP status, or 0 when no response arrived. |
| `Type`, `Code` | The error type and the code of the API, such as `validation_error` and `invalid_parameter`. |
| `Param` | The field that is to blame, when there is one. |
| `RequestID` | The id to quote when you write to support. |
| `DocURL` | The page of the docs that explains the code. |
| `RetryAfter` | How long the API asked you to wait. |
| `Fields` | The fields a sign-up form refused, each with a `Key` and an `Error`. |
| `Body` | The decoded response body. |

## The kinds of failure

| Sentinel | When it matches |
| --- | --- |
| `ErrAuthentication` | The key or token was refused, with a 401. |
| `ErrPermission` | The credential may not do this, with a 403. |
| `ErrScopeMissing` | The credential lacks the scope the call needs. |
| `ErrStepUpRequired` | An access token has to verify a code first. |
| `ErrNotFound` | Nothing has that id, with a 404. |
| `ErrConflict` | The change conflicts with the current state, with a 409. |
| `ErrValidation` | A field was refused, with a 422. |
| `ErrRateLimited` | Too many requests, or an allowance is spent, with a 429. |
| `ErrInvalidRequest` | Any other refusal of the request. |
| `ErrServer` | The API failed, with a status of 500 or more. |
| `ErrRetryable` | The status is one the client retries. |
| `ErrNetwork` | No response arrived. |
| `ErrTimeout` | No response arrived before the timeout. |
| `ErrInvalidArgument` | The call itself was wrong, and nothing was sent. |
| `ErrWebhookSignature` | A webhook delivery failed its check. |

A cancelled context still matches `context.Canceled`, and a deadline that passed matches `context.DeadlineExceeded`.

## What is tried again

- Reads, sends and every write that is safe to repeat are tried again, up to `WithMaxRetries` times. Any other write is sent once.
- The statuses 408, 500, 502, 503 and 504 are retried with a backoff that starts at half a second and doubles up to eight seconds.
- A 429 is retried only when it carries a `Retry-After` of a minute or less, and the client waits that long.
- A connection that fails or times out is retried the same way.

A send that is retried carries the same idempotency key every time, so the API replays the first message instead of sending a second one.
