Skip to main content

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

CodeCause
missing_api_key, invalid_api_key, restricted_api_key401 / 403
not_found404
validation_error, missing_required_field, invalid_argument400
method_not_allowed, conflict405 / 409
rate_limit_exceeded, tier_limit_exceeded429
service_unavailable, internal_server_error5xx
connection_error, timeout_errorTransport
application_errorCatch-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}`,
});