Skip to content

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.

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. false means 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.ConflictError
switch {
case err == nil:
case errors.As(err, &conflict):
rebase(conflict.ActualETag) // your function
case 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 access
default:
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.

Failures before the server answers are ordinary Go errors with Status(err) == 0:

  • client.ErrClosed: the client was closed.
  • no preferred servers configured, or no 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.

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”.

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.

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.