Error Handling
Every service method returns a plain error. Transport-level failures (DNS,
connection refused, timeout) come back wrapped in fmt.Errorf; anything the
Z.AI API itself rejected comes back as *client.APIError, with structured
fields you can branch on instead of parsing message strings.
resp, err := c.Chat().Create(ctx, req)
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) {
fmt.Printf("[%d] %s\n", apiErr.Code, apiErr.UserMessage)
switch apiErr.Category {
case client.ErrorCategoryAuth:
// bad/expired key — don't retry, tell the user
case client.ErrorCategoryQuota:
// out of balance/quota — don't retry, surface to the user
case client.ErrorCategoryRateLimit:
if apiErr.IsRetriable {
// the client already retries this internally up to
// Config.MaxRetries — you'll only see it here if retries
// were exhausted or disabled (MaxRetries: -1)
}
}
return
}
// transport-level failure — network, DNS, timeout
}
APIError fields:
| Field | Meaning |
|---|---|
HTTPStatus |
HTTP status code |
Code |
Z.AI's business error code (int) |
Message |
Raw message from the API |
Category |
One of the categories below |
UserMessage |
A friendlier, pre-written description |
IsRetriable |
Whether the client's own retry logic considers this transient |
RequestID |
For support/debugging, when the API returned one |
Helper predicates: IsAuthError(), IsRateLimitError(), IsQuotaError(),
IsParameterError(), IsServerError() — equivalent to checking .Category
directly, provided for readability at call sites.
Retry behavior you get for free
Client.doRequest (used by every service) already retries 429/5xx/network
errors with exponential backoff, jitter, and Retry-After support, up to
Config.MaxRetries (default 3; set to -1 to disable). You generally don't
need your own retry loop — APIError.IsRetriable tells you whether an error
that reached your code already exhausted those retries.
Error code reference
| Code | Constant | Category | Retriable |
|---|---|---|---|
| 1000 | ErrCodeAuthFailed |
Auth | No |
| 1001 | ErrCodeAuthNotFound |
Auth | No |
| 1003 | ErrCodeAuthTokenExpired |
Auth | No |
| 1005 | ErrCodeAuthNeed2FA |
Auth | No |
| 1113 | ErrCodeInsufficientBalance |
Quota | No |
| 1302 | ErrCodeRateLimitReached |
RateLimit | Yes |
| 1305 | ErrCodeServiceOverloaded |
Server | Yes |
| 1308 | ErrCodeUsageLimitReached |
Quota | No |
| 1309 | ErrCodeCodingPlanExpired |
Quota | No |
| 1310 | ErrCodeWeeklyMonthlyExhausted |
Quota | No |
| 1311 | ErrCodeModelNotIncluded |
Quota | No |
| 1313 | ErrCodeFairUsageViolation |
Quota | No |
| 1314 | ErrCodeEnterpriseExpired |
Quota | No |
| 1315 | ErrCodeEnterpriseKeyOnly |
Quota | No |
| 1316–1321 | usage-limit variants | Quota | No |
| 1210 | ErrCodeInvalidParameter |
Parameter | No |
| 1211 | ErrCodeUnknownModel |
Parameter | No |
| 1212 | ErrCodeMethodNotSupported |
Parameter | No |
| 1213 | ErrCodeParameterMissing |
Parameter | No |
| 1214 | ErrCodeParameterInvalid |
Parameter | No |
| 1215 | ErrCodeParametersConflict |
Parameter | No |
| 1221 | ErrCodeAPITakenOffline |
Parameter | No |
| 1222 | ErrCodeAPINotExist |
Parameter | No |
| 1261 | ErrCodePromptTooLong |
Parameter | No |
| 1301 | ErrCodeUnsafeContent |
Content | No |
| 1220 | ErrCodeNoPermission |
Permission | No |
| -1 | ErrCodeInternalError |
Server | Yes |
| 1200 | ErrCodeAPICallError |
Server | Yes |
| 1230 | ErrCodeProcessError |
Server | Yes |
| 1234 | ErrCodeNetworkError |
Server | Yes |
An error code this client hasn't seen before defaults to ErrorCategoryServer
with IsRetriable: true — a reasonable default (treat unknowns as transient
server issues) but the constants above are the ones with a specifically
tailored UserMessage and retry decision.
Source of truth: pkg/client/errors.go.
The 200-with-embedded-failure quirk
A few endpoints (Agents' Invoke/AsyncResult) return HTTP 200 even when the
call fails at the business level — the failure is embedded in the response
body, not signaled via HTTP status. A non-nil error from these methods
means the transport failed; check resp.Failed() (or resp.Error/
resp.Status directly) for a business-level failure inside a successful
response. Both response types document this on their Failed() method — it's
easy to miss if you only check err != nil.