Error handling
Every SDK method rejects on failure. Nothing returns null to signal an error.
import { ApiError, AuthError, NetworkError, MemorySodaError,} from '@memory-soda/sdk';Hierarchy
Section titled “Hierarchy”Error└── MemorySodaError base, catch this to catch everything ├── ApiError the server responded with a non-2xx ├── AuthError 401 or 403 └── NetworkError the request never completedApiError
Section titled “ApiError”class ApiError extends MemorySodaError { readonly status: number; // HTTP status readonly body: unknown; // parsed JSON body, or null}try { await memory.getThread(threadId);} catch (err) { if (err instanceof ApiError && err.status === 404) { // gone, or belongs to another project, indistinguishable by design }}body is usually { error: string }, and { error, issues } for validation
failures.
AuthError
Section titled “AuthError”Thrown for 401 and 403 before ApiError is considered, so an auth failure is
never an ApiError.
class AuthError extends MemorySodaError { // message defaults to 'Invalid or missing API key'}Causes: wrong key, revoked key, key not linked to a project, missing
Authorization header. Not retryable, it is a configuration problem.
NetworkError
Section titled “NetworkError”class NetworkError extends MemorySodaError { readonly networkCause?: unknown; // the original fetch error / AbortError}Covers DNS failure, connection refused, TLS problems and client timeouts.
A timeout surfaces here, not as an ApiError, because no response arrived.
if (err instanceof NetworkError) { const aborted = (err.networkCause as Error)?.name === 'TimeoutError';}Status codes
Section titled “Status codes”| Status | Error | Means |
|---|---|---|
| 400 | ApiError |
Validation failed, body.issues has the zod detail |
| 401 | AuthError |
Missing, invalid or revoked key |
| 403 | AuthError |
, |
| 404 | ApiError |
Thread, fact or episode not found, or not yours |
| 409 | ApiError |
Conflict (dashboard user creation) |
| 500 | ApiError |
Server error, safe to retry |
| 503 | ApiError |
/health when a dependency is down |
Full list: Errors.
Handling by kind
Section titled “Handling by kind”import { ApiError, AuthError, NetworkError } from '@memory-soda/sdk';
async function safeRecall(dataset: string, query: string): Promise<string> { try { const { context } = await memory.recall({ dataset, query }); return context; } catch (err) { if (err instanceof AuthError) { throw err; // config problem, fail loudly } if (err instanceof NetworkError) { logger.warn({ err }, 'memory unreachable'); return ''; // degrade: answer without memory } if (err instanceof ApiError && err.status >= 500) { logger.error({ status: err.status }, 'memory server error'); return ''; } throw err; }}The principle: memory should never take your product down. An answer without memory beats no answer.
Retrying
Section titled “Retrying”Nothing is retried automatically. Retry 5xx and network failures; never retry
4xx.
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> { let lastErr: unknown; for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (err) { lastErr = err; const retryable = err instanceof NetworkError || (err instanceof ApiError && err.status >= 500); if (!retryable || i === attempts - 1) throw err; await new Promise((r) => setTimeout(r, 2 ** i * 250 + Math.random() * 100), ); } } throw lastErr;}
const { context } = await withRetry(() => memory.recall({ dataset, query }));Idempotency
Section titled “Idempotency”| Operation | Safe to retry? |
|---|---|
recall, prepare, get, list*, health |
Yes, pure reads |
threads.create |
Creates a new thread each time |
addMessage |
Appends a duplicate message |
threads.end |
Creates another episode (harmless, costs 3 LLM calls) |
compact |
No-op when nothing to compact |
deleteFact |
404 on the second call |
There are no idempotency keys. For writes, retry only on NetworkError where you
have reason to believe the request never landed, and accept that a timeout after
the server committed will duplicate.
Timeouts
Section titled “Timeouts”Set once on the client, applied per request:
const memory = new MemorySoda({ baseUrl, apiKey, timeout: 15_000 });Defaults to 60 seconds, deliberately generous, because two operations are slow:
| Operation | Can take |
|---|---|
addMessage that triggers auto-compaction |
up to ~30 s |
recall({ include: ['synthesis'] }) |
1–3.5 s |
A tight global timeout will cut those off. Use two clients if you want a short timeout on the hot path:
const fast = new MemorySoda({ baseUrl, apiKey, timeout: 5_000 });const slow = new MemorySoda({ baseUrl, apiKey, timeout: 60_000 });Validation errors
Section titled “Validation errors”try { await memory.addMessage(threadId, { role: 'user', content: '' });} catch (err) { if (err instanceof ApiError && err.status === 400) { console.log(err.body); // { error: 'Validation error', // issues: [{ code: 'too_small', minimum: 1, path: ['content'], … }] } }}issues is the raw zod issue array. Useful in development; do not surface it to
end users.
Unknown fields are stripped, not rejected. A typo in an optional field name is silently ignored rather than returning 400.
Logging
Section titled “Logging”function describe(err: unknown) { if (err instanceof ApiError) return { kind: 'api', status: err.status, body: err.body }; if (err instanceof AuthError) return { kind: 'auth' }; if (err instanceof NetworkError) return { kind: 'network', cause: String(err.networkCause) }; return { kind: 'unknown', message: String(err) };}Do not log err.body for reads, it can contain user content.
- Errors reference, every endpoint’s failure modes
- Type reference