Errors
Branch on typed errors. Read the request id you put in support tickets, and let the SDK retry the transient ones.
Every failed request throws a typed error. Each one extends MedblocksError, carries the same set of fields, and maps to one HTTP status, so you branch on the specific case with instanceof and let the base class catch the rest.
import { MedblocksError } from "@medblocks/connect";
try {
const session = await mb.patientSession.init(input);
} catch (err) {
if (err instanceof MedblocksError) {
// every API error lands here, typed
console.error(err.code, err.requestId);
}
throw err;
}Error types
The hierarchy has one base class and eight subclasses, each pinned to a type and an HTTP status. Import the ones you handle and check them with instanceof.
| Class | Status | When it fires |
|---|---|---|
MedblocksError | any | The base class. Catch it as a fallback. |
MedblocksAuthenticationError | 401 | Missing, invalid, or expired API key. |
MedblocksPermissionError | 403 | The key is valid but not allowed here, for example a cross-org read or a missing scope. |
MedblocksInvalidRequestError | 400 | A field failed validation. param names it. |
MedblocksNotFoundError | 404 | The resource doesn’t exist for your organization. |
MedblocksConflictError | 409 | A duplicate external_id or a state conflict. |
MedblocksRateLimitError | 429 | Over quota. retryAfter holds the wait. |
MedblocksEhrError | 502 | An upstream service failed, such as the EHR, OAuth, or storage. |
MedblocksApiError | 500 | An unexpected error on the Medblocks side. |
A transport failure that outlives the SDK’s retries is the one exception. A dropped connection, a DNS failure, or a timeout surfaces as the runtime’s own error, such as a TypeError or an abort, not a MedblocksError. That’s why the example above re-throws what it doesn’t recognize.
MedblocksSignatureError is separate. It comes from webhook verification, never from an API call, and has no HTTP status. See Webhook signature errors.
Error fields
Every MedblocksError instance carries the same fields, parsed from the API’s error envelope.
| Field | Type | What it is |
|---|---|---|
message | string | A human-readable explanation. |
code | string | A stable, machine-readable code. Branch on this. |
type | string | The error category, one of the type values above. |
param | string | null | The offending field, when known. |
requestId | string | Correlation id from the X-Request-Id header. Put it in support tickets. |
statusCode | number | The HTTP status. |
docUrl | string | A link to the docs for this code. |
MedblocksRateLimitError adds one field, retryAfter. It’s the wait in seconds parsed from the Retry-After header, or null if the server didn’t send one.
Handling errors
Catch the cases you treat differently, and let the base class catch everything else. Order matters, so check the subclasses before MedblocksError.
import {
MedblocksAuthenticationError,
MedblocksInvalidRequestError,
MedblocksRateLimitError,
MedblocksError,
} from "@medblocks/connect";
try {
await mb.patientSession.init(input);
} catch (err) {
if (err instanceof MedblocksAuthenticationError) {
// 401, alert on a bad or expired key
} else if (err instanceof MedblocksInvalidRequestError) {
console.error("invalid", { param: err.param, code: err.code });
} else if (err instanceof MedblocksRateLimitError) {
// 429, wait err.retryAfter seconds
} else if (err instanceof MedblocksError) {
console.error("medblocks error", { code: err.code, requestId: err.requestId });
} else {
throw err; // a native error, such as a network failure
}
}The base MedblocksError also covers any error type a future API version adds. A new type arrives as a base instance instead of slipping past your instanceof checks.
Common codes
Each class carries a stable code you branch on for the exact reason. These are the ones you’ll see most often.
| Code | Class | Meaning |
|---|---|---|
missing_api_key | MedblocksAuthenticationError | No API key on the request. |
invalid_api_key | MedblocksAuthenticationError | The key is wrong or revoked. |
expired_api_key | MedblocksAuthenticationError | The key aged out. |
insufficient_scope | MedblocksPermissionError | The key lacks the scope this call needs. |
bad_request | MedblocksInvalidRequestError | A field failed validation. |
unsupported_api_version | MedblocksInvalidRequestError | The pinned Version isn’t supported on this key. |
resource_not_found | MedblocksNotFoundError | No such resource in your organization. |
external_id_already_exists | MedblocksConflictError | An external_id is already taken in your org. |
throttled | MedblocksRateLimitError | Too many requests. |
quota_exceeded | MedblocksRateLimitError | The key’s quota is spent. |
oauth_error | MedblocksEhrError | The hospital’s OAuth provider failed. |
fhir_error | MedblocksEhrError | The upstream FHIR service failed. |
internal_error | MedblocksApiError | An unexpected error on the Medblocks side. |
The full list lives in the API errors reference.
Webhook signature errors
Medblocks.webhooks.constructEvent verifies an incoming webhook delivery before it hands you the event. When verification fails it throws MedblocksSignatureError. This is purely client-side, so the error has no statusCode and no network call was made. Read reason to tell the failure modes apart.
reason | What it means |
|---|---|
missing_header | No Medblocks-Signature header on the request. |
malformed_header | The header didn’t match t=<sec>,v1=<hex>. |
timestamp_expired | The signature timestamp is outside the tolerance window, 5 minutes by default. |
signature_mismatch | The HMAC didn’t match, so the wrong secret was used or the body was tampered with. |
malformed_body | The body wasn’t valid JSON in the event envelope shape. |
Catch it and return 400, since a delivery that fails verification is one you don’t trust. constructEvent is async, so await it.
import { Medblocks, MedblocksSignatureError } from "@medblocks/connect";
try {
const event = await Medblocks.webhooks.constructEvent(
rawBody, // the unparsed request body, as a string
req.headers["medblocks-signature"],
process.env.MEDBLOCKS_WEBHOOK_SECRET,
);
// event.type is narrowed, event.data.object is typed by it
} catch (err) {
if (err instanceof MedblocksSignatureError) {
console.error("bad webhook signature", { reason: err.reason });
return new Response("invalid signature", { status: 400 });
}
throw err;
}Pass the raw, unparsed body. Verification runs an HMAC over the exact bytes, so a re-serialized JSON object won’t match. The webhook signatures page covers the full setup.
Logging
The request id is the most useful field in a support ticket, because it lets us find your exact call. Log it on every error path.
import { MedblocksError } from "@medblocks/connect";
catch (err) {
if (err instanceof MedblocksError) {
logger.error("medblocks api error", {
type: err.type,
code: err.code,
requestId: err.requestId,
statusCode: err.statusCode,
param: err.param,
});
}
throw err;
}Retries
The SDK retries transient failures for you before it throws. That covers rate limits (429), upstream errors (502, 503, 504), and network-level failures. It doesn’t retry other 4xx errors or 500, since retrying those won’t help. By the time an error reaches your catch, it has already been retried up to maxNetworkRetries times, three by default.
On a 429, the SDK honors the server’s Retry-After during its own retries. If those run out, the final MedblocksRateLimitError still carries retryAfter for your own backoff.
