Documentation Index
Fetch the complete documentation index at: https://docs.omophub.com/llms.txt
Use this file to discover all available pages before exploring further.
Errors Are Return Values
Unlike SDKs that throw on API errors, every method in @omophub/omophub-node returns a discriminated { data, error, meta, headers } union. Network failures, 4xx, and 5xx all surface as { data: null, error, headers }. TypeScript narrows data to the success type in the else branch:
const { data, error } = await client.concepts.get(999_999_999);
if (error) {
console.error(error.name, error.message, error.statusCode);
return;
}
// `data` is now typed as Concept
console.log(data.concept_name);
The only thing the SDK throws is OMOPHubError on constructor misuse (missing API key) and OMOPHubIteratorError from async iterators (see below).
Error Shape
interface ErrorResponse {
name: OMOPHUB_ERROR_CODE_KEY; // stable code suitable for switch statements
message: string;
statusCode: number | null; // HTTP status, or null for client-side errors
requestId?: string; // from x-request-id header
retryAfter?: number; // seconds, populated on 429 / 503 / 502 / 504
details?: Record<string, unknown>;
}
Error Codes
| Code | Cause |
|---|
missing_api_key, invalid_api_key, restricted_api_key | 401 / 403 |
not_found | 404 |
validation_error, missing_required_field, invalid_argument | 400 |
method_not_allowed, conflict | 405 / 409 |
rate_limit_exceeded, tier_limit_exceeded | 429 |
service_unavailable, internal_server_error | 5xx |
connection_error, timeout_error | Transport |
application_error | Catch-all |
Body-supplied error codes (from error.code or error.name in the response body) win over the generic status mapping - so a 400 carrying { error: { code: 'missing_required_field' } } surfaces as missing_required_field, not the generic validation_error.
Narrowing by Code
const { data, error } = await client.concepts.get(999_999_999);
if (error) {
switch (error.name) {
case 'not_found':
console.log('Concept missing - falling back to source vocabulary');
break;
case 'rate_limit_exceeded':
console.log(`Retry after ${error.retryAfter}s`);
break;
case 'invalid_api_key':
throw new Error('Refresh your OMOPHub API key');
default:
console.error(error.message, error.requestId);
}
}
Synthetic Validation Errors
The SDK validates a few invariants client-side before issuing the request - concepts.batch (1–100 ids), mappings.map (exactly one of sourceConcepts/sourceCodes, non-empty), search.similar (exactly one of conceptId/conceptName/query), fhir.resolveBatch (1–100 codings), fhir.resolveCodeableConcept (1–20 codings).
These return a synthetic error with statusCode: null and never hit the network:
const { error } = await client.concepts.batch({ conceptIds: [] });
console.log(error?.name); // 'validation_error'
console.log(error?.statusCode); // null
Retries
The client automatically retries on:
- HTTP 429, 502, 503, 504
- Transient network errors (DNS failures, connection drops)
Backoff is full-jitter exponential: min(500 * 2^attempt, 8000) * (1 - 0.25 * random()). The Retry-After header is honoured up to a 60 s cap and floored at 100 ms (so Retry-After: 0 doesn’t become a spam-retry).
const client = new OMOPHub('oh_xxx', {
maxRetries: 3, // default
timeoutMs: 30_000,
});
// Disable retries entirely
const noRetry = new OMOPHub('oh_xxx', { maxRetries: 0 });
POST/PATCH Idempotency
To prevent accidental duplicate writes on retry, POST and PATCH only retry when an Idempotency-Key is set. Idempotent verbs (GET/HEAD/OPTIONS/PUT/DELETE) always retry.
// Will NOT retry on 503 - POST without an idempotency key
await client.concepts.batch({ conceptIds: [1, 2] });
// WILL retry on 503 - opt in via idempotencyKey
await client.concepts.batch({
conceptIds: [1, 2],
idempotencyKey: 'my-pipeline-run-2026-05-30-batch-1',
});
Timeout
The per-request timeout defaults to 30 s. On expiry the SDK returns a timeout_error (it does not retry timeouts):
const { error } = await client.search.basic('diabetes');
if (error?.name === 'timeout_error') {
// Either the network stalled or the server was too slow
}
// Disable
const noTimeout = new OMOPHub('oh_xxx', { timeoutMs: 0 });
AbortSignal
Pass an AbortSignal via the per-call options to cancel a request mid-flight. Caller-initiated aborts re-throw as AbortError - they’re a deliberate cancellation, not an API error:
const controller = new AbortController();
setTimeout(() => controller.abort(), 100);
try {
await client.search.basic('diabetes', { signal: controller.signal });
} catch (e) {
if (e instanceof Error && e.name === 'AbortError') {
console.log('User cancelled');
}
}
Async Iterators
*Iter variants (search.basicIter, search.semanticIter) are async generators. Generators can’t gracefully yield discriminated errors, so they throw OMOPHubIteratorError on page failure:
import { OMOPHubIteratorError } from '@omophub/omophub-node';
try {
for await (const concept of client.search.basicIter('diabetes')) {
console.log(concept.concept_id);
}
} catch (e) {
if (e instanceof OMOPHubIteratorError) {
console.error(`Page failed (${e.code}, status ${e.statusCode}): ${e.message}`);
}
}
Prefer the eager *All variant if you want errors as values:
const { data, errors, pagesFetched } = await client.search.basicAll('diabetes', {
pageSize: 100,
maxPages: 10,
});
console.log(`Collected ${data.length} concepts across ${pagesFetched} pages`);
if (errors.length > 0) {
console.warn('Partial result:', errors);
}
Headers and Request IDs
Every response - success or error - includes the wire headers as a plain Record<string, string>:
const { headers } = await client.concepts.get(201826);
console.log(headers?.['x-request-id']);
console.log(headers?.['x-ratelimit-remaining']);
error.requestId is also populated automatically for support ticket triage.
Custom Fetch (Proxy / Instrumentation)
Pass your own fetch implementation for proxying, logging, or test injection:
const instrumented: typeof fetch = async (url, init) => {
console.log(`→ ${init?.method ?? 'GET'} ${url}`);
const t0 = Date.now();
const response = await fetch(url, init);
console.log(`← ${response.status} (${Date.now() - t0}ms)`);
return response;
};
const client = new OMOPHub('oh_xxx', { fetch: instrumented });
Best Practices
Switch on error.name, Not Strings
if (error?.name === 'rate_limit_exceeded') {
await sleep((error.retryAfter ?? 1) * 1000);
// retry
}
Log error.requestId for Support
if (error) {
logger.warn('OMOPHub error', {
code: error.name,
requestId: error.requestId,
statusCode: error.statusCode,
});
}
Set Idempotency Keys for Write-Like POSTs
await client.mappings.map({
targetVocabulary: 'SNOMED',
sourceConcepts: [201826],
idempotencyKey: `${runId}-${batchIndex}`,
});