Skip to content
skyl

Errors

Eight sentinels, one Error type, and errors.Is all the way down.

skyl classifies every provider failure onto one of eight sentinels and wraps it in an *Error carrying enough context to act on. You branch on the classification, never on message text — providers reword their messages, and string matching breaks silently when they do.

The sentinels#

SentinelMessageRetried?Meaning
ErrAuthskyl: authentication failedNeverThe credential was missing, malformed, or rejected. The same key will fail again.
ErrRateLimitskyl: rate limitedYesThe provider is throttling. Retried with backoff, honouring Retry-After.
ErrNotFoundskyl: not foundNeverThe model or endpoint does not exist for this account. Because model IDs pass through unvalidated, a typo arrives here rather than failing locally.
ErrBadRequestskyl: invalid requestNeverThe request was malformed. Often produced locally by Request.Validate.
ErrServerskyl: provider server errorYesThe provider failed on its side. Retried with backoff.
ErrUnsupportedskyl: unsupported by this providerNeverThis provider cannot express part of the request. Returned instead of silently dropping data, because a quietly discarded image looks like a model that ignored the question.
ErrRefusalskyl: model declined the requestNeverThe model or its safety classifiers declined. The same prompt gets the same answer.
ErrStreamClosedskyl: stream is closedNeverThe stream was used after being closed.

Caveats

  • Branch with errors.Is. The sentinels are stable; provider messages are not.
  • Unwrap returns a slice — the sentinel and the underlying cause — so errors.Is(err, context.DeadlineExceeded) works on the same value that matches errors.Is(err, skyl.ErrServer).
  • An Error never contains credentials. Body is the provider's payload truncated to 2 KB.
  • Local validation failures are ErrBadRequest too, so one branch catches both yours and the provider's.

The API#

SymbolPurpose
Sentinel errorsThe eight values you branch on
ErrorThe type carrying provider, status, message and retry hint
Error.RetryableWhether retrying could plausibly succeed
Error.UnwrapReturns the sentinel and the cause
NewErrorBuilds a classified provider error
ClassifyStatusMaps an HTTP status onto a sentinel
ParseRetryAfterInterprets a Retry-After header
UnsupportedfBuilds an ErrUnsupported naming what failed

The last four are exported so that an adapter in your repository produces errors indistinguishable from an in-tree one.

Usage#

Branching

goCompiles
resp, err := client.Complete(ctx, req)
switch {
case err == nil:
case errors.Is(err, skyl.ErrRateLimit):
	// Already retried with backoff; it kept failing. Shed load.
case errors.Is(err, skyl.ErrAuth):
	return fmt.Errorf("credential rejected: %w", err)
case errors.Is(err, skyl.ErrRefusal):
	// Never retried — the same prompt gets the same answer.
default:
	return err
}
resp, err := client.Complete(ctx, req)
switch {
case err == nil:
case errors.Is(err, skyl.ErrRateLimit):
	// Already retried with backoff; it kept failing. Shed load.
case errors.Is(err, skyl.ErrAuth):
	return fmt.Errorf("credential rejected: %w", err)
case errors.Is(err, skyl.ErrRefusal):
	// Never retried — the same prompt gets the same answer.
default:
	return err
}

Recovering the detail

goCompiles
var e *skyl.Error
if errors.As(err, &e) {
	log.Printf("%s returned %d: %s (retryable=%v)",
		e.Provider, e.StatusCode, e.Message, e.Retryable())
}
var e *skyl.Error
if errors.As(err, &e) {
	log.Printf("%s returned %d: %s (retryable=%v)",
		e.Provider, e.StatusCode, e.Message, e.Retryable())
}

Troubleshooting#

My error handling broke after a provider changed its wording

You were matching on message text. Branch on the sentinels with errors.Is; that is what they are for.

I cannot tell a timeout from a DNS failure

Both arrive with StatusCode == 0 and no sentinel — but the cause survives:

goCompiles
if errors.Is(err, context.DeadlineExceeded) { /* we ran out of time */ }
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() { /* a transport timeout */ }
if errors.Is(err, context.DeadlineExceeded) { /* we ran out of time */ }
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() { /* a transport timeout */ }

Edit this page on GitHub