Docs/SDK · @ewyn/client/Errors & retries

Errors & retries

The SDK retries transient failures automatically and surfaces everything else as a typed EwynApiError. Idempotency keys make the whole pipeline safe to retry.

v1 · beta

Handling errors

typescript
import { Ewyn, EwynApiError } from '@ewyn/client';

try {
  await client.send({ /* ... */ });
} catch (error) {
  if (error instanceof EwynApiError) {
    console.error('API Error:', error.status, error.message);
    console.error('Details:', error.details);

    if (error.status === 408) {
      // Request timed out (error.code === 'TIMEOUT')
    }
    if (error.isRetryable()) {
      // 429 or 5xx — the SDK already retried this automatically
    }
  }
}
Property / methodMeaning
statusHTTP status code; 408 indicates a client-side timeout
codeOptional error code, e.g. 'TIMEOUT' or 'NETWORK_ERROR'
detailsAdditional payload from the API (e.g. validation issues)
isClientError()true for 4xx responses
isServerError()true for 5xx responses
isRetryable()true for 5xx and rate-limit 429s — what the SDK retries automatically. Quota 429s (QUOTA_EXCEEDED, ATTACHMENT_QUOTA_EXCEEDED) are not retried
isSuppressed()true when the recipient is blocked for this send (422 RECIPIENT_SUPPRESSED) — a delivery block or a marketing opt-out
isOptedOut()true when the block is a marketing opt-out (the recipient unsubscribed from the topic or from all marketing); a transactional send with the same type would go through
suppressionDetails(){ kind, reason, topic?, messageId } for a blocked send, else undefined

Blocked recipients

A send to a blocked recipient fails with 422 and is not retryable — the block has to be removed first. The attempt is still recorded, so the message appears in your dashboard with a Suppressed status and can be sent manually from there if it was blocked by mistake. Blocked sends do not count against your quota.

suppressionDetails().kind tells the two cases apart: 'suppression' is a delivery block (HARD_BOUNCE and MANUAL stop every send, COMPLAINT stops marketing sends), and 'opt_out' is a marketing unsubscribe (OPT_OUT_TOPIC with the topic slug, or OPT_OUT_ALL). Opt-outs never block a send with type: 'transactional'.

typescript
try {
  await client.send({ type: 'marketing', topic: 'newsletter', /* ... */ });
} catch (error) {
  if (error instanceof EwynApiError && error.isSuppressed()) {
    const { kind, reason, topic, messageId } = error.suppressionDetails() ?? {};
    // kind:   'suppression' | 'opt_out'
    // reason: 'HARD_BOUNCE' | 'MANUAL' | 'COMPLAINT' | 'OPT_OUT_TOPIC' | 'OPT_OUT_ALL'
    // topic:  the slug, for OPT_OUT_TOPIC
    if (error.isOptedOut()) {
      // They unsubscribed; don't resend marketing. A receipt or reset for the
      // same person still goes through with type: 'transactional'.
    }
    console.log(`Not sent (${reason}). Review message ${messageId} in the dashboard.`);
  }
}
Older servers.

suppressionDetails() also reads the flat reason that servers before v0.15 answered with, and derives kind from it; an UNSUBSCRIBE reason counts as an opt-out.

Temporary bounces don't suppressRecipients are only suppressed by permanent bounces. A temporary failure — a full mailbox, a greylisting server — records the bounce but leaves the recipient sendable.

Automatic retries

The SDK retries on:

  • 429 Too Many Requests — rate limits only; a 429 with code QUOTA_EXCEEDED or ATTACHMENT_QUOTA_EXCEEDED is not retried
  • 5xx server errors
  • Network errors

Retries use exponential backoff with a 1-second base delay. With the default maxRetries: 3 the SDK makes up to 3 total attempts (initial + 2 retries) with delays of 1s and 2s. Timeouts (408) and other 4xx errors are not retried.

Idempotency

Provide an idempotencyKey on sends so retries — yours or the SDK's — can never produce duplicate emails:

typescript
await client.send({
  to: 'user@example.com',
  subject: 'Your receipt',
  templateId: 'template-version-uuid',
  type: 'transactional',
  idempotencyKey: `order-confirmation-${order.id}`,
  variables: { total: order.total },
});
24-hour window.

A repeated idempotencyKey within 24 hours returns the original response instead of sending again. If the original send terminally failed, the retry re-queues it — you never get silent loss. The one exception is a failed send whose attachments have since been deleted (6-month retention): the replay returns 410 ATTACHMENTS_EXPIRED and you should send again with a new key.

Wondering what the server enforces (rate limits, validation, status codes)? See API errors & rate limits.

Was this page helpful?