BlobError. Most bad options and arguments are a TypeError instead, a ValueError in Python, and in Go an error returned where the option is written; see TypeScript, Python, Go. It carries a code from a fixed list, a status, and a message you can show to a user. The same codes are used by every server SDK, in the browser, and inside the React hooks, so an error raised on your server reaches the browser with its code intact.
Catching it and reading its fields: TypeScript, Python, Go.
The codes
The status is what an upload route answers with.
Bad option values are not in this list. An unparseable
'5mbit', a missing token, a route with no before-upload callback: those throw where the option is written, at startup, not as a BlobError per request. What they throw in each SDK: TypeScript, Python, Go.
Extra fields
Some codes carry extra details. Their names and types in each SDK: TypeScript, Python, Go.Messages
Hints fold into the message
signature_mismatch and length_required have a built-in hint. Its text in each SDK: TypeScript, Python, Go.
Errors in the browser
app/picker.tsx
error.code inside a hook is the code your server raised, not a status number you have to decode.
What reaches the browser, in order
A route runs its error callback first. If it returns a response, that is the answer; if it returns aBlobError, the answer is that error’s JSON at its own status. Otherwise the throw falls through three cases:
- A
BlobErroris answered as itself, at its own status, with its hint, failed paths, etag, size and retry-after when they are set. - An app error carrying a
statusbetween 400 and 599 is mapped through the table below. This is how an auth check that throws its own 401 reaches the browser asunauthorized. - Anything else is treated as your bug and rethrown, so your framework logs it with its stack rather than masking it as a generic 500.
StatusCode() int method on the error, and case 3 has nothing to rethrow to: the handler logs the error on its Logger and answers a 500 with no detail. See Refusing.
Any other status becomes
request_failed, keeping the status it arrived with.
A throw out of the completion callback also deletes the completed object before the error is answered. See Recording the upload.
What storage errors map to
Storage is Cloudflare R2. Its errors are normalized before they leave the SDK, first matching wins.
For that last row the status is passed through, except that a 5xx becomes 502, since the failure is upstream of your app rather than in it.
Errors the browser raises on its own
Some failures never reach your route, so the browser names them itself. A PUT that fails with no status and no bytes sent was refused by the browser before it went out, and the reason is never visible to script because it is the preflight that failed:signature_mismatch. A 401 or 403 on an older URL is read as an expired signature and the browser asks the route for a new one instead. See Retries.
Exhausted retries become request_failed, carrying the attempt count and the last status, hinted with what to do next:
AbortError, not a BlobError. The record’s status is canceled and it carries no error at all, so a cancel never renders as a failure.
Platform body limits
useServerUpload and any route of your own that the bytes pass through. It does not apply to direct browser uploads.
Vercel caps a serverless request body at 4.5MB, AWS Lambda at 6MB, and Cloudflare at 100MB on the free plan. A file over that cap is refused with a 413 that never reached your route, so it carries no code of its own. The SDK turns it into too_large and attaches the cap as a hint:
Credential errors
The SDK waits out short backoffs itself.
mint_backoff is a pause too long for one request to wait through, handed back to you instead of holding a serverless invocation open.
Credentials are short-lived and re-minted before they expire. One that expires mid-request is handled inside the SDK, so only a second refusal surfaces. See How signing works.