Error handling
Catch and inspect ApiError, handle session expiry with autoRefresh, and resolve optimistic concurrency conflicts.
#Error handling
Every SDK method that hits the API throws an ApiError when the server returns a non-ok response. This page explains the error object, session expiry handling, and concurrency conflicts.
#The ApiError class
ApiError extends Error and carries the structured fields from the API's error envelope:
| Property | Type | Description |
|---|---|---|
code |
string |
Machine-readable error code (e.g. validation_error, not_found) |
status |
number |
HTTP status code from the response |
message |
string |
Human-readable description |
details |
unknown |
Optional structured details — for validation errors, a fields object with per-field messages |
import { EmuView, ApiError } from '@emuview/sdk';
const sdk = new EmuView({
url: 'https://your-api.example.com',
auth: { apiKey: 'sk-your-api-key' }
});
try {
await sdk.collection('products').create({ title: '' });
} catch (err) {
if (err instanceof ApiError) {
console.error(err.code); // "validation_error"
console.error(err.status); // 422
console.error(err.message); // "One or more fields failed validation."
console.error(err.details); // { fields: { title: "Title is required." } }
} else {
throw err; // Network failure or a bug — not an API response
}
}
instanceof ApiError checks work reliably — the class restores its prototype chain after super(). err.toString() formats as ApiError [code] (status): message.
#How it works
The SDK parses the API's JSON error envelope { error, message, details } into the ApiError. If the response body isn't valid JSON (a gateway timeout, for example), code falls back to UNKNOWN_ERROR and message to the HTTP status text.
#Common error codes
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized |
Missing or invalid auth token |
| 403 | forbidden |
Insufficient permissions for this action |
| 404 | not_found |
Record, collection, or resource doesn't exist |
| 409 | already_exists |
Unique constraint violation or duplicate resource |
| 412 | precondition_failed |
Optimistic concurrency check failed |
| 422 | validation_error |
Request body failed field validation |
| 429 | rate_limited |
Too many requests — retry after a delay |
| 500 | internal_server_error |
Server error |
The error reference lists every code the API can return.
#Handling by code
Branch on code (stable) rather than parsing message (may change):
import { ApiError } from '@emuview/sdk';
try {
await sdk.collection('orders').get('rec_01HXK5M');
} catch (err) {
if (err instanceof ApiError) {
switch (err.code) {
case 'not_found':
showEmptyState();
break;
case 'forbidden':
showAccessDenied();
break;
case 'rate_limited':
await new Promise((r) => setTimeout(r, 2000));
break;
default:
reportError(err);
}
}
}
#Session expiry
Session tokens expire. Two client options control what happens when a request comes back 401:
| Option | Default | Behaviour |
|---|---|---|
autoRefresh |
false |
On a 401 with a session token, re-validate the session via auth.getSession() and retry the original request once |
onSessionExpired |
— | Called when the session is truly expired and the retry is not possible |
import { EmuView } from '@emuview/sdk';
const sdk = new EmuView({
url: 'https://your-api.example.com',
auth: { sessionToken: storedToken },
autoRefresh: true,
onSessionExpired: () => {
// Session is gone — send the user back to the login page
window.location.href = '/login';
}
});
sdk.auth.getSession() itself never throws for auth failures — it returns null when the user isn't signed in (401 or 403) and only throws for network or server errors.
#Concurrency conflicts
update() supports optimistic concurrency control. Pass the record's _version as the expected version; the server returns 409 if someone else modified the record in the meantime.
Use _version, not updated_at: _version is a counter the server bumps on every write, while updated_at has second precision and was the version source in an older design. And expect 409, not 412 — update() sends the version in the request body, and only the If-Match header channel answers 412.
import { ApiError } from '@emuview/sdk';
const record = await sdk.collection('products').get('rec_01HXK5M');
try {
await sdk
.collection('products')
.update('rec_01HXK5M', { price: 29.99 }, { version: record._version });
} catch (err) {
if (err instanceof ApiError && err.status === 409) {
// Re-fetch, re-apply your change, and retry
const fresh = await sdk.collection('products').get('rec_01HXK5M');
await sdk
.collection('products')
.update('rec_01HXK5M', { price: 29.99 }, { version: fresh._version });
}
}
A rejected update writes nothing, so retrying against the fresh _version is safe. When several clients hold the same version, exactly one commits.
#Gotchas
- Not every failure is an
ApiError. Network failures (DNS, offline, CORS) reject with the runtime's ownTypeErrorfromfetch. Checkinstanceof ApiErrorbefore readingcode. DELETEreturns nothing. Successful deletes resolve toundefined— an error is signalled by a throw, not a return value.- Rate limits on auth endpoints fail closed. Sign-in endpoints are limited to 5 requests per minute per IP; back off when you see
rate_limited.
#Related concepts
- Error reference — the complete code catalogue and response format
- Installation and setup — where
autoRefreshandonSessionExpiredare configured - SDK collections — the CRUD methods these errors come from