Handling errors
Every failure is a SwarmhitError; branch on its class and its code.
When a request fails, the method throws. Every error the SDK throws extends
SwarmhitError, so one catch handles them all. The error's class tells you
what kind of failure it was.
import { APIError, NotFoundError, RateLimitError } from '@swarmhit/swarmhit-sdk';
try {
const campaign = await swarmhit.campaigns.get(campaignId);
} catch (err) {
if (err instanceof NotFoundError) {
// the campaign does not exist (or was deleted)
} else if (err instanceof RateLimitError) {
console.log('limit reached', err.details);
} else if (err instanceof APIError) {
console.error(err.status, err.code, err.message, err.details);
} else {
throw err; // no answer (network, timeout), or a bug in your code
}
}Error classes
The API answered with an error. These extend APIError:
| Class | Status | Typical cause |
|---|---|---|
BadRequestError | 400 | A missing or invalid field, or a rule the request breaks (no seat left). Some missing records are a 400 with code not-found too |
AuthenticationError | 401 | A missing, revoked or mistyped key, or an X-Swarmhit-Workspace the key cannot act on |
PaymentRequiredError | 402 | A purchase (mailboxes, domains, seats) whose payment failed, or no card on file |
PermissionDeniedError | 403 | Not allowed for this workspace or the key's creator: a feature not enabled, billing rights missing |
NotFoundError | 404 | The resource does not exist in this workspace |
ConflictError | 409 | The current state forbids it: a paused sender, an invitation already pending, the 25-webhook cap, an idempotency key still in use |
UnprocessableEntityError | 422 | LinkedIn could not act on that profile or post |
RateLimitError | 429 | A sender's daily or weekly LinkedIn limit, or LinkedIn throttling the sender |
InternalServerError | 5xx | A failure on our side (500), or LinkedIn or the mail provider failing behind the request (502, 503) |
Other failures. These extend SwarmhitError but not APIError:
| Class | When |
|---|---|
APIConnectionError | The request never got an answer: network, DNS or TLS failure. |
APITimeoutError | The timeout ran out (it extends APIConnectionError). |
APIUserAbortError | Your signal aborted the request. |
WebhookVerificationError | A webhook delivery failed verification (see Webhooks). |
Before it throws a connection error, a timeout, a 5xx or a rate-limit 429,
the SDK has already retried the request when that was safe.
What an APIError carries
| Property | |
|---|---|
status | The HTTP status. |
code | A stable, machine-readable reason, such as seat-limit-reached, inmail_credits_exhausted or not-found. When the API sends none, a generic one for the status (invalid_request for a 400, not_found for a 404). |
message | A sentence for humans. Its wording may change: don't parse it. |
details | Extra context when there is some, for example { limit, used, scope, resetAt } on a sender's limit. |
headers | The response headers. |
RateLimitError adds retryAfter: the number of seconds to wait, when the
response carries a Retry-After header. A sender's limit says when it resets
in details instead (see Sender limits).
Branch on the code
Use err.code to handle a specific situation, rather than err.message. It
never changes within an API version:
async function sendInMail(senderId, profileUrl, subject, text) {
try {
return await swarmhit.accounts.inmails.send(senderId, { profileUrl, subject, text });
} catch (err) {
if (!(err instanceof APIError)) throw err;
switch (err.code) {
case 'inmail_credits_exhausted':
return queueForNextMonth(profileUrl);
case 'profile_not_found':
return markInvalid(profileUrl);
case 'sender_paused':
return notifyOwner(senderId);
default:
throw err;
}
}
}Some codes you are likely to meet:
| Code | Meaning |
|---|---|
unauthorized | The key is missing, wrong or revoked. |
not-found, not_found | No such resource in this workspace. |
invalid_request | The generic code of a 400 without a more specific one; message says what is wrong. |
seat-limit-reached | Connecting a sender needs a free seat. |
profile_not_found | The LinkedIn profile URL or identifier could not be resolved. |
sender_paused | The sender is paused; resume it first. |
inmail_credits_exhausted | The sender has no InMail credits left. |
idempotency_in_progress, idempotency_failed | See Retries. |
The Getting started guide describes the HTTP errors in more detail.
Sender limits
LinkedIn actions count against the sender's daily and weekly limits. Over a
limit, the API answers 429. The error's details say which limit was
reached ({ limit, used, scope, resetAt }, scope being day, week or
pending) and when it resets:
try {
await swarmhit.accounts.connections.send(senderId, { profileUrl });
} catch (err) {
if (err instanceof RateLimitError && err.details?.resetAt) {
await scheduleAt(err.details.resetAt, () => sendInvite(profileUrl));
} else {
throw err;
}
}A 429 without details comes from LinkedIn throttling the sender: back off.
The SDK never retries these on its own: a retry within seconds can't help,
and hurts a throttled account.