Skip to content

Error Handling and Reliability

Igor Sazonov edited this page Sep 23, 2026 · 1 revision

Every service method returns an error that can be classified with the standard library's errors.As and errors.Is. The client distinguishes invalid configuration, missing Pro credentials, non-success HTTP responses, rate limits, network failures, and JSON decode failures. URLs stored in these errors are redacted when a Pro key is involved.1

Error types

Type When it occurs Useful fields
*ConfigError New receives invalid configuration. Msg
*ProAPIKeyRequiredError A Pro method is called without WithAPIKey. No request is built. Route
*NotFoundError The API returns HTTP 404. Embedded *APIError
*RateLimitError The API returns HTTP 429. RetryAfter, embedded *APIError
*APIError The API returns another non-2xx response. StatusCode, URL, Header, Body
*TransportError Network, DNS, TLS, timeout, or cancellation failure. URL, Err; it unwraps to the underlying cause.
*DecodeError A successful response is not one complete valid JSON value. URL, Err; it unwraps to the decode error.

APIError.Body is limited to 4096 bytes. The library also redacts the configured Pro key from error URLs, headers, bodies, and nested standard-library URL errors before returning them.1

Classify errors with errors.As

Handle the situations that have different recovery actions before returning a general error.

_, err := client.TVL().GetProtocol(ctx, "not-a-real-protocol")

var (
    notFound  *defillama.NotFoundError
    rateLimit *defillama.RateLimitError
    apiErr    *defillama.APIError
    transport *defillama.TransportError
    decode    *defillama.DecodeError
    missingKey *defillama.ProAPIKeyRequiredError
)

switch {
case errors.As(err, &notFound):
    // The resource does not exist.
case errors.As(err, &rateLimit):
    // RetryAfter is zero when the server supplied no usable Retry-After value.
    return fmt.Errorf("rate-limited; retry after %s: %w", rateLimit.RetryAfter, err)
case errors.As(err, &apiErr):
    // Another HTTP error: inspect StatusCode, Header, and Body.
case errors.As(err, &transport):
    // errors.Is/As can reach the root cause through TransportError.
case errors.As(err, &decode):
    // The upstream success body was not valid JSON.
case errors.As(err, &missingKey):
    // Configure the client with WithAPIKey before this call.
case err != nil:
    return err
}

Keep an appropriate context deadline even where retry is enabled. If a context is cancelled or reaches its deadline while waiting to retry, the client returns promptly.2

Configure bounded retries

Retries are disabled by default. Set MaxAttempts greater than one to retry eligible GET failures:

client, err := defillama.New(defillama.WithRetryPolicy(defillama.RetryPolicy{
    MaxAttempts: 3,
    BaseDelay:  250 * time.Millisecond,
    MaxDelay:   2 * time.Second,
    Jitter:     true,
}))

Only the following errors are retryable: transient transport errors, *RateLimitError for HTTP 429, and *APIError with a 5xx status. The client does not retry cancellation, ordinary 4xx responses, permanent transport errors, or DecodeError. Its backoff doubles from BaseDelay, is capped by MaxDelay, may include ±50% jitter, and waits at least as long as a valid server Retry-After when necessary.2

MaxAttempts counts the initial request. Therefore MaxAttempts: 1 means no retry.

Context, concurrency, and client lifetime

The Client has immutable configuration after construction, holds no request-level state, and is safe to share across goroutines. Create it once for a given configuration and use a context suitable for each call.

client, err := defillama.New(defillama.WithTimeout(10 * time.Second))
if err != nil {
    return err
}

ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
prices, err := client.Prices().GetCurrentPrices(ctx, []string{"coingecko:bitcoin"})

WithTimeout configures only the SDK-created HTTP client. If the application injects a client with WithHTTPClient, it owns that client's timeout and transport settings. The request context can always impose a tighter deadline.3

Production checklist

Use one client per configuration, a bounded context per request, and an explicit retry policy only where retries fit the calling workload. Branch on NotFoundError and RateLimitError rather than parsing error strings. Never log the raw API-key source, even though returned SDK errors redact the key. Convert float64 financial transport values before calculations that require exact decimal semantics.

References

Clone this wiki locally