Errors
This page shows how to read what went wrong. Every failure the server classifies reaches you as a typed error carrying its status code, so you can tell “slow down” from “the mailbox is full” from “you may not do that” without parsing text.
Sentinels and types
Section titled “Sentinels and types”Check coarse conditions with errors.Is, and use errors.As when you need the details:
| Sentinel | Type | Status | Meaning | Retry? |
|---|---|---|---|---|
client.ErrRateLimited |
*client.RateLimitedError |
429 | This peer exceeded its allowance. | After RetryAfter. |
client.ErrOverloaded |
*client.OverloadedError |
503 | The server could not admit the request in time. | Yes, with jitter. |
client.ErrConflict |
*client.ConflictError |
409 | An If-Match precondition failed. ActualETag holds the current ETag. |
Only after re-reading and merging. |
client.ErrMailboxFull |
*client.MailboxFullError |
507 | A mailbox, a feed or the server’s storage is at its cap. | No: someone must make room. |
client.ErrForbidden |
*client.ForbiddenError |
403 | The resource exists and you may not do this to it. | No. |
client.ErrNotFound |
*client.NotFoundError |
404 | The resource does not exist on this server. | No. |
Each typed error matches its own sentinel and no other. Any other status (400, 500) arrives as a plain
*client.ProtocolError, which has Op, Status, Message and RetryAfter. The typed errors embed it.
Two helpers read any of them:
client.Status(err)returns the status code, or 0 if the error did not come from a classified server response (a dial failure, a timeout, a decoding error).client.RetryAfter(err)returns the server’s wait hint and whether it gave one.falsemeans it did not say; back off on your own terms rather than retrying at once.
client.IsRetryable(err) is true for rate limiting and overload only. A full mailbox and a conflict are
deliberately not retryable: waiting does not fix them, and retrying them on a timer loops forever.
var conflict *client.ConflictErrorswitch {case err == nil:case errors.As(err, &conflict): rebase(conflict.ActualETag) // your functioncase client.IsRetryable(err): wait, ok := client.RetryAfter(err) if !ok { wait = time.Second } time.Sleep(wait)case errors.Is(err, client.ErrForbidden): // ask the owner for accessdefault: log.Printf("status %d: %v", client.Status(err), err)}A rate limit’s RetryAfter is exact: it is when the next token arrives, so waiting longer only slows
you down. An overload’s is a floor; add jitter so a fleet of clients does not retry in step.
Errors that are not from the server
Section titled “Errors that are not from the server”Failures before the server answers are ordinary Go errors with Status(err) == 0:
client.ErrClosed: the client was closed.no preferred servers configured, orno preferred server reachable:with each attempt’s error.- Context cancellation and deadlines, wrapped.
- Client-side limits, such as a batch over 100 messages, refused before sending.
Results that carry the outcome
Section titled “Results that carry the outcome”Some calls return a result instead of an error for an outcome that is normal for them.
Submissions. SendMessage returns an error only when the request failed. A refused message comes
back as SendResult{Success: false}; res.Err() gives the typed error. SendMessages does the same per
message with BatchSendResult.Err(), and returns an error only when the whole request failed.
res, err := cl.SendMessage(ctx, to, payload)if err == nil { err = res.Err()}if errors.Is(err, client.ErrMailboxFull) { // tell the user; the recipient has to drain their inbox}Batch document writes. PutDocuments reports each document’s own Status, and ActualETag on a
409. Check r.OK() for each result.
GetDocument. It reports the server’s status in DocumentResponse.Status for 304, 403 and 404, with
IsNotModified, IsForbidden, IsNotFound and IsSuccess, so a conditional get can see a 304. The
other document calls return typed errors.
Flag and delete acks. UpdateFlags on a message you do not own returns Success == false with
message not found in the ack. MarkDelivered and DeleteMessages skip IDs that are not yours and
report what they acted on in UpdatedCount and DeletedCount.
Deletes return false only for “already gone”
Section titled “Deletes return false only for “already gone””DeleteDocument, DeleteFeed, DeleteCollection and DeleteCollectionItem return:
| Outcome | Return |
|---|---|
| Deleted | true, nil |
| Did not exist | false, nil |
| Refused or failed | false, err |
A refusal or a server fault is always an error, never “deleted”.
Lookups that return nil
Section titled “Lookups that return nil”GetFeed, GetFeedEntry, GetCollection, GetCollectionItem and GetDirectoryEntry return nil, nil
when the resource does not exist, and a typed error for a refusal. GetFeedEntries on a missing feed
returns an untyped error whose text is feed not found.
Mailbox management
Section titled “Mailbox management”The mailbox management calls (CreateMailbox, GrantAccess, …) return typed errors when the server
classifies the failure. Some refusals, such as a grant on a folder that does not exist, arrive as a plain
*client.ProtocolError with status 500 and the server’s message, such as mailbox not found. Branch on
the sentinel where there is one and log the message otherwise. GetMailboxInfo reports a failure as an
untyped error with the server’s message.
The status codes and the wire envelope they travel in are listed in Wire format & status codes.