Errors
Every failure throws. Two classes, and a request id on every API error.
Catching one
import { OpenEmailApiError, OpenEmailNetworkError, openemail } from '@openemail/sdk' try { await openemail.emails.send(message)} catch (error) { if (error instanceof OpenEmailApiError) { if (error.isValidation) console.error(error.code, error.param, error.message) if (error.isPermission) console.error(await openemail.addresses.list()) if (error.isRateLimited) console.error('try again in', error.retryAfterSeconds, 'seconds') console.error(error.status, error.requestId) } if (error instanceof OpenEmailNetworkError && error.isTimeout) console.error('no answer in time') throw error}A permission_error on a send is usually the KEY’s send scope, a domain or an address it was not given, rather than the workspace, which is why the sample prints what addresses.list() says this key may send as.
The classes
| Class | When |
|---|---|
| `OpenEmailApiError` | The API answered, and not with a success. Carries status, type, code, param, docUrl, requestId and retryAfterSeconds. |
| `OpenEmailNetworkError` | No response arrived: DNS, TLS, a dropped connection, the timeout, or your own AbortSignal. Carries cause, and isTimeout is true when the timeout was the reason. |
| `Error` | Thrown before anything is sent: a missing or malformed key, an unusable baseUrl, a browser, an empty id. |
| Getter | True when |
|---|---|
| `isAuth` | type is authentication_error, a 401: no key, the wrong kind of credential, or a key we did not issue. |
| `isPermission` | permission_error, a 403: a real key without the scope or the From address it needs. |
| `isScopeMissing` | code is insufficient_scope, the 403 that names a missing scope. |
| `isInvalidRequest` | invalid_request_error, a 400: a request that could not be understood. A message over the size ceiling comes back as a 422 message_too_large, so isValidation is the getter that catches it. |
| `isValidation` | validation_error, a 422: the schema refused it, and param names the field. |
| `isNotFound` | not_found_error, a 404: no such resource. |
| `isConflict` | conflict_error, a 409: the resource is past the point where this could be done to it. |
| `isRateLimited` | rate_limit_error, a 429. retryAfterSeconds holds the wait when the server named one. |
| `isServerError` | status is 500 or above. Quote requestId if you contact support. |
| `isRetryable` | status is 408, 429, 500, 502, 503 or 504. |
The getters read type, the frozen half of the envelope. code stays a string, because the API guarantees it is open and additive, so treat one you do not recognise as its type. A closed union would make an SDK upgrade the price of reading a new failure mode.
A body that is not the API’s error envelope still becomes an OpenEmailApiError, with type inferred from the status and code set to unrecognised_response. A success whose body is not JSON throws one too.
An abort is an OpenEmailNetworkError as well, whether it lands during the request or during the wait before a retry, and the abort is kept on cause. Check signal.aborted when you need to tell your own cancellation from a network failure.
requestId
Every OpenEmailApiError carries the request id the server sent, from the error body or the x-request-id header, and it is the only thing tying your failure to a line in the server's log. A success resolves to the parsed body alone, so there is no request id to read on one.