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 own TypeError from fetch. Check instanceof ApiError before reading code.
  • DELETE returns nothing. Successful deletes resolve to undefined — 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.