Topic 5.6
Errors, Retries and Abort in the SDK
In one line
The SDK retries some failures for you (maxRetries, default 2), reports a final failure as an APICallError or a RetryError, and sends errors that happen while streaming as masked error chunks. You decide what the browser may read, turn status codes into plain sentences, and keep partial answers when a stream is stopped or breaks.
Think of it like this
A courier who finds nobody home. A good courier tries again a couple of times (retries). If it still fails, they leave a card that says what happened in plain words, 'We called twice, nobody answered, here is how to rebook', and they do not read out the customer's account details at the door (masking). If you cancel the delivery halfway, they keep the parcel they already unpacked (a partial answer). The SDK does the retries and the card; you write the words on it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Retryable error
- A failure that might succeed if tried again soon: statuses 408, 409, 429 and 5xx. The SDK sets
isRetryableon them. - Backoff
- Waiting longer before each retry, so a struggling service is not hit again at once.
- RetryError
- The SDK error (
AI_RetryError) thrown when every retry failed. It holdserrorsandlastError. - Error chunk
- A stream event
{"type":"error","errorText":"…"}that reports a failure inside a 200 response. - Masking
- Replacing a real error message with a safe one before it reaches the browser.
- Reference id
- A short id such as
req-12shown to the user and logged next to the real error, so support can find it. - friendly(error)
- Your one function that turns any client error into a sentence for a person.
- Partial message
- An assistant message that holds only the text that arrived before a stop or a failure.
Step by step
01Retries and what survives them
The first example counts how many times the model was really called. A call that fails with a 429 and then a 503 succeeds on the third attempt with maxRetries: 2. A call that keeps failing with a 429 ends in a RetryError after maxRetries + 1 attempts. A 401 is not retried at all.
The stand-in sets retry-after: 0 so the example does not wait; a real provider's header makes the SDK wait that long, and the user waits with it.
02Throwing and not throwing
The second example runs the same failing model through generateText (it throws) and streamText (it does not). It prints what onError received, what the text stream held, what awaiting result.text did, and the parts of fullStream.
03Say less on purpose
The third example is a route with a custom onError that writes a short sentence plus a reference id and logs the real error, and a browser function friendly(error). It runs five failures: one inside the stream, a stream that breaks, your own 429 with the wait in the body, a 401, and an unreachable URL. The last column shows the exact sentence each would put on screen.
04Stopped and broken answers
The fourth example stops an answer after its third word (the script waits for the third word to arrive, not for a time), shows what stayed in messages, then runs a stream that breaks and shows that the partial text stays too. regenerate() then replaces the broken answer with a complete one.
Try it yourself
- 1
Retry once more than the failures
In the first example change the first call's
maxRetries: 2tomaxRetries: 1.The two failures need two retries, so the call now throws
AI_RetryError: Failed after 2 attempts. Last error: …status 503instead of recovering, and the example stops there with an uncaught error. Count your provider's typical hiccups before choosing the number. - 2
Return the raw message
In the third example change the server's
onErrortoreturn error.message;and run it.The 'provider429' line now ends with the text from the provider, including the key fragment
sk-live-DEMO. That is exactly the leak masking prevents. - 3
Print error.message directly
In the third example replace
friendly(chat.error)in the last line withchat.error.message.The 401 and the own 429 now print raw JSON such as
{"error":"signed_out"}, and the offline case printsFailed to fetch. That is what a person would see without a mapping function.
Code & diagrams
maxRetries counts the extra tries: maxRetries 1 means up to two calls. The user is waiting through every attempt, so a large number makes the thinking state long.
Preview
Console
Nothing logged yet.Expected console output
1. "Recovered." after 3 calls
2. AI_RetryError | reason: maxRetriesExceeded | attempts: 2 | last status: 429 | calls: 2
3. AI_APICallError | status: 401 | retryable: false | calls: 1The error is in the stream, not in an exception. If your route only has try/catch around streamText, it will never run.
Preview
Console
Nothing logged yet.Expected console output
generateText threw: AI_APICallError 500
onError got: AI_APICallError 500 - upstream exploded
streamText did not throw. pieces read: 0
awaiting result.text threw: AI_NoOutputGeneratedError
--- the same failure as stream parts
start
error 500Five failures, five sentences, no key fragment on screen. The (server log) lines show where the real messages went. 'own429' reads the wait from the JSON body because the client error has no response headers.
Preview
Console
Nothing logged yet.Expected console output
(server log) req-1 429 Rate limit reached for key sk-live-DEMO
provider429 -> Mitra is busy right now. Reference: req-1
(server log) req-2 500 upstream reset
cut -> Mitra had a problem. Reference: req-2
own429 -> You are asking too fast. Please wait 30 seconds.
signedout -> Please sign in again.
offline -> You seem to be offline. Check your connection.A stopped answer and a broken answer both leave their partial text behind. Their text parts stay in the streaming state, so the UI cannot tell from the part that the answer is unfinished: track 'stopped' or 'cut off' yourself.
Preview
Console
Nothing logged yet.Expected console output
after stop: status ready | last: assistant | text part state: streaming | partial text kept: true | full text: false
after break: status error | last: assistant | text part state: streaming | partial text kept: true | text: "Orders ship in"
after regenerate: status ready | last: assistant | text part state: done | partial text kept: true | text: "Orders ship in 1 to 2 working days."Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Leak the provider's message
In the third example change the server's onError to return error.message;.
Myth vs fact
Myth
If maxRetries is high, my users never see errors.
Fact
They see a long 'thinking' state instead, and a stream that fails halfway is not retried at all. Retries cover only calls that fail before any output.
Myth
A 200 status means the answer is complete.
Fact
A stream can fail after the 200 is sent. Look for the error chunk (useChat does it for you) and at the finish reason.
Myth
Masking errors makes debugging impossible.
Fact
Masking applies to what the browser sees. Your server log still has everything, tied to a reference id you can ask the user for.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Retry policy is a product decision. A chat answer that needs 20 seconds because of three retries may be worse than telling the user at once and offering a button. Many teams use
maxRetries: 1for chat and higher values for background jobs. - ▸
Retries repeat the whole request, including its cost if the provider already billed for an input. Do not retry calls that change something outside (Topic 1.4); with tools (Phase 8) the retry applies to the model call, not to a tool that already ran.
- ▸
Log the
AI_RetryError.errorsarray: the first failure is often different from the last, and the first one is the real cause. - ▸
isRetryablefor a 429 does not tell you whether the limit is per minute (retry soon) or a spent daily quota (retry tomorrow). Read the provider's message on the server and tell the user the difference (Topic 3.6).
Remember this
- 1
The problem this solves: There are four places where an AI call can fail: before the request leaves (bad setup), at the provider (429, 5xx, 401), on the way (offline), and in the middle of a stream. Each reaches your code in a different shape, and a careless handler either shows the user a raw provider message (with account details in it) or hides everything behind 'Something went wrong'. This topic maps each shape to what you should do.
- 2
Retries in the SDK.
generateTextandstreamTextretry a call that fails with a retryableAPICallError(status 408, 409, 429 or 5xx) up tomaxRetriestimes, 2 by default, waiting longer each time and respecting aretry-afterheader. A non-retryable error such as a 401 is thrown at once, after one call. When retries run out you get anAI_RetryError(reason: "maxRetriesExceeded") witherrors(every attempt) andlastError. The user waits through all of it, with the status stuck onsubmitted, so a highmaxRetriesmakes the 'thinking' state long. Set it deliberately, and0when your own code does the retrying (Topic 1.4). - 3
Two kinds of failure on the server.
generateTextthrows, so you wrap it intry/catch.streamTextdoes not throw: it returns at once, and a failure goes to theonErrorcallback and into the stream as anerrorchunk (Topic 5.2). Readingresult.textafter a failed stream throwsAI_NoOutputGeneratedError;result.textStreamsimply ends with nothing in it. - 4
What the browser may read. By default the error chunk says only 'An error occurred.' To say more, return a string from
onErrorintoUIMessageStreamResponse({ onError }). That string is whatuseChatputs inerror.message. Use it to return a short, safe sentence and a reference id, and log the real error with the same id on the server. **Never returnerror.messageas is**: provider messages can include key fragments, account ids, or prompt text. - 5
Two shapes on the client. A failure inside the stream becomes the string you returned from
onError(anErrorwhose message is that text). A failure *status* from your route (401, 429, 500, 502, 413) becomes anAI_APICallErrorwhosestatusCodeis the status and whosemessageis the response body text, often JSON, sometimes an HTML error page from a proxy. Offline is aTypeError: Failed to fetch. Write one functionfriendly(error)that checks these three in order and returns a sentence; never printerror.messagedirectly. The client error does not carry the response headers, so if you want a countdown, put the seconds in the JSON body. - 6
Abort and partial messages.
stop()keeps what arrived. A broken stream keeps what arrived too, and moves toerror. In both cases the partial text is inmessages, with its text part in thestreamingstate. Mark such an answer in the UI ('Stopped' or 'Cut off') so nobody mistakes half a sentence for a finished one, and offer Continue orregenerate(). When you save chats, store the partial text with a flag (Topic 5.8).
Explain it without notes
How does maxRetries work, and which errors does the SDK retry?
Why can't you catch a streamText failure with try/catch, and where do you handle it?
How do you make sure a provider's error message never reaches the browser, and what does the user see instead?
What state is a stopped or broken answer left in, and how should the UI show it?
Practice
Write friendly(error) so that a 413 shows 'That message is too long.' and a 503 shows 'Mitra is down for maintenance. Please try again soon.'
A teammate wraps streamText in try/catch and logs 'failed' there. Explain why that never prints, and show the correct place.
Trade-offs
- ↔
Generous retries hide short outages but make users wait and add load to a provider that is already struggling. A low value shows errors sooner but needs a good retry button. Pick per feature.
- ↔
Detailed error text helps users fix their own problems (for example 'message too long') but any detail from the provider can leak. Write your own messages for the cases you know and use a generic one plus a reference id for the rest.
Done when you can
I can explain
maxRetries,isRetryableandAI_RetryError.I can handle failures of
generateText(catch) andstreamText(onErrorand the stream).I can write an
onErrorthat returns a safe sentence and a reference id, and log the real error.I can write
friendly(error)for stream errors, status errors and offline.I can keep a partial answer after a stop or a break and mark it as unfinished.