Skip to content
SMS SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Errors

Every SMS SDK error class with its code, retrySafe value, when it is thrown, and what to do about it.

Every error SMS SDK throws extends SmsError. Each has a stable code to switch on and a retrySafe flag that tells you whether you can send the same logical message again without risking a duplicate.

retrySafe: true means nothing was accepted. Either the SDK never sent a request, or the provider proved it did not create the message. It does not mean a retry will succeed. retrySafe: false means a retry could duplicate the message, or the error is not about sending at all.

import { HandoffUnknownError, isSmsError } from "@opencoredev/sms-sdk";
import { sms } from "./sms";

try {
  await sms.send({ to: "+14155550123", body: "Your order has shipped.", idempotencyKey: "order:123:shipped:v1" });
} catch (error) {
  if (!isSmsError(error)) throw error;
  console.error(JSON.stringify(error)); // redacted: name, code, message, retrySafe, attempts, provider
  if (error instanceof HandoffUnknownError) {
    // reconcile with the provider before sending again
  } else if (error.retrySafe) {
    // safe to send again later
  }
}

Summary

Class code retrySafe What to do
ConfigurationError configuration true Fix the options. Happens at startup.
InvalidRecipientError invalid_recipient true Fix the number format. Do not retry as is.
InvalidSenderError invalid_sender true Pass from or configure the adapter’s default, in a valid format.
InvalidMessageError invalid_message true Fix the field named in field.
UnsupportedFieldError unsupported_field true Remove the field or use an adapter that supports it.
PolicyRejectedError policy_rejected true Respect your policy. Do not retry until it allows the send.
ProviderRejectedError provider_rejected true Act on category.
ProviderAuthError provider_auth true Fix the provider credentials.
ProviderRateLimitedError provider_rate_limited true Retry later, after retryAfterMs if set.
AllProvidersRejectedError all_providers_rejected true Inspect errors, one per adapter.
HandoffUnknownError handoff_unknown false Do not resend. Reconcile with the provider first.
SendAbortedError aborted true Send again when ready.
IdempotencyConflictError idempotency_conflict false Use a new key for a different message.
IdempotencyInProgressError idempotency_in_progress true Retry later. You get the other call’s result.
WebhookSignatureError webhook_signature false Respond 401 or 403. Do not process the request.
WebhookPayloadError webhook_payload false Respond 400 and log. The request was authentic but incomplete.

createSmsClient and the adapter factories throw ConfigurationError. parseSmsWebhook throws the two webhook errors. send() throws the rest.

SmsError

The base class. Every error has these fields.

Field Type Meaning
name string The class name, such as "HandoffUnknownError".
code SmsErrorCode Stable machine-readable code.
message string Human-readable explanation. Phone numbers are masked.
retrySafe boolean See above.
attempts readonly SendAttempt[] Provider requests made before the error, oldest first. Empty when nothing was sent.
provider ProviderErrorInfo | undefined Redacted provider details: { provider, httpStatus?, code?, message?, requestId? }. message has numbers masked and is at most 200 characters.
cause unknown The original exception, when there was one. For debugging only.

toJSON() returns { name, code, message, retrySafe, attempts, provider? }. It never includes cause, message bodies, credentials, or full phone numbers, so JSON.stringify(error) is safe to log.

isSmsError(value) is a type guard for any SDK error.

Configuration

ConfigurationError

  • code: configuration. retrySafe: true.
  • Thrown by createSmsClient for no adapters, duplicate adapter names, or a non-positive timeoutMs, and by adapter factories for malformed credentials (a Twilio Account SID that is not AC plus 32 hex, an empty API key, a Vonage private key that is not a PEM).
  • Fix the configuration. Nothing was sent.

Local validation

send() throws these before any provider request, so nothing was sent. sms.validate() reports the same problems as issues without throwing.

InvalidRecipientError

  • code: invalid_recipient. retrySafe: true.
  • to does not match ^\+[1-9]\d{1,14}$.
  • Normalize the number to E.164. See Senders and E.164 numbers.

InvalidSenderError

  • code: invalid_sender. retrySafe: true. Extra field: providerName?: string.
  • No sender on the message or the adapter, or a malformed sender. Malformed means a sender ID that is not 1 to 11 letters, digits, or spaces with at least one letter, a short code that is not 3 to 8 digits, a Twilio Messaging Service SID that is not MG plus 32 hex, or a Telnyx alphanumeric sender without messagingProfileId.
  • Fix the sender or the adapter’s from.

InvalidMessageError

  • code: invalid_message. retrySafe: true. Extra field: field: ValidationField.
  • An empty body without media, any empty body on Vonage, a relative or non-http media or webhook URL, an invalid sendAt date, a validityPeriodSec that is not a positive integer or is outside the adapter’s range, an idempotencyKey outside 1 to 255 characters, a provider limit such as Twilio’s 1600-character body or 10 media URLs, or a providerOptions value of the wrong type or out of range.
  • Fix the named field.

UnsupportedFieldError

  • code: unsupported_field. retrySafe: true. Extra fields: field: ValidationField, providerName: string.
  • The primary adapter cannot send mediaUrls, sendAt, validityPeriodSec, webhookUrl, or the sender type. Thrown before any request, even with fallback on.
  • Also thrown, with field: "providerOptions", when any configured adapter’s providerOptions entry has a key it does not accept, an extra key that names a parameter the SDK sets, or the adapter accepts no options at all.
  • Remove the field, change the sender, or put an adapter that supports it first. See MMS, scheduling, and validity.

Policy

PolicyRejectedError

  • code: policy_rejected. retrySafe: true. Extra field: reason: string.
  • Your beforeSend returned { kind: "reject", reason }. Nothing was sent and fallback never runs.
  • message is always “Send blocked by the beforeSend policy.” Your reason may name a person or number, so it stays on error.reason only. It is not in message, toJSON(), hook payloads, or the idempotency store.
  • Respect the decision. An error thrown inside beforeSend is not wrapped in this class. It propagates as is.

Provider rejections

The provider answered and proved it did not create the message. Sending the same message again cannot create a duplicate.

ProviderRejectedError

  • code: provider_rejected. retrySafe: true.
  • Extra fields: category: RejectionCategory, providerName: string, fallbackEligible: boolean, and provider with the provider’s HTTP status, error code, message, and request ID.
  • What to do depends on category.
category Falls back Typical cause What to do
auth Yes Bad or revoked credentials Fix credentials. Thrown as ProviderAuthError.
rate_limited Yes, after retries Throttling or a full queue Retry later. Thrown as ProviderRateLimitedError.
sender Yes Sender not provisioned, not registered, or not allowed for this destination Fix the sender or its registration.
account Yes Balance, trial limits, geographic permissions, spend limit Fix the account settings with the provider.
recipient No Invalid or unreachable number Mark the number bad. Another provider would fail too.
compliance Never The recipient opted out, or the content was blocked Add the number to your suppression list. Never route around it.
request No Any other 4xx Inspect error.provider and fix the request.

isFallbackEligible(category) returns the “Falls back” column. Per-provider code mappings are on each provider page.

ProviderAuthError

  • code: provider_auth. Extends ProviderRejectedError with category: "auth".
  • Fix the credentials. With fallback on, the next adapter was tried.

ProviderRateLimitedError

  • code: provider_rate_limited. Extends ProviderRejectedError with category: "rate_limited". Extra field: retryAfterMs: number | undefined.
  • Thrown after same-adapter retries ran out, or when Retry-After was longer than retry.maxDelayMs.
  • Retry later, after retryAfterMs when set. Raise retry.maxAttempts if this is common.

AllProvidersRejectedError

  • code: all_providers_rejected. retrySafe: true. Extra field: errors: readonly ProviderRejectedError[], one per attempted adapter, in order.
  • Fallback was on and every attempted adapter rejected, the last one with a non-eligible category or with no adapters left. When only one adapter was tried, its ProviderRejectedError is thrown directly instead.
  • Act on each entry in errors. A compliance entry means the recipient opted out.

Unknown outcome

HandoffUnknownError

  • code: handoff_unknown. retrySafe: false. Extra fields: reason: HandoffUnknownReason, providerName: string | undefined.
  • SMS SDK cannot tell whether the provider accepted the message. The client never retries it and never falls back.
reason Cause
network The connection failed after the request may have been sent.
timeout No response within timeoutMs.
aborted Your signal aborted while the request was in flight.
server_error The provider returned a 5xx.
unexpected_status A 1xx or 3xx status, or a 429 without the provider’s documented rate-limit error.
malformed_response A 2xx without the documented message ID, or a body that is not JSON.
adapter_exception The adapter threw instead of returning an outcome.
replayed_unknown An earlier send with this idempotency key had an unknown outcome.
stale_reservation An earlier send with this key never finished and is older than staleReservationSec.

Check whether the message exists before sending again. Look it up in the provider’s console or message API around the time of attempts, or wait for a status webhook. If it was never created, clear the idempotency record (store.delete(key) on the memory store) and send again. See Idempotency.

SendAbortedError

  • code: aborted. retrySafe: true.
  • Your signal aborted before any request was sent, or while waiting to retry after a proven rate-limit rejection.
  • Send again when ready. An abort during a request throws HandoffUnknownError instead.

Idempotency

IdempotencyConflictError

  • code: idempotency_conflict. retrySafe: false. Extra field: idempotencyKey: string.
  • The key was already used with a different to, from, body, mediaUrls, sendAt, validityPeriodSec, webhookUrl, or providerOptions. Nothing was sent.
  • Use a new key for a new message, such as a version suffix (:v2). Retrying with the same key and payload will keep failing.

IdempotencyInProgressError

  • code: idempotency_in_progress. retrySafe: true. Extra field: idempotencyKey: string.
  • Another process holds a reservation for the key that is younger than staleReservationSec. This call sent nothing.
  • Retry later with the same key. You get the other call’s outcome. That is its result, a new attempt if it was rejected, or HandoffUnknownError.

Webhooks

WebhookSignatureError

  • code: webhook_signature. retrySafe: false. Extra fields: reason: WebhookFailureReason, providerName: string.
  • reason is one of missing_signature, invalid_signature, stale_timestamp, body_hash_mismatch, malformed_signature, invalid_credentials.
  • Respond with 401 or 403 and do not process the request. A steady stream of invalid_signature usually means a wrong credential or a URL mismatch behind a proxy. See Webhook security.

WebhookPayloadError

  • code: webhook_payload. retrySafe: false. Extra field: providerName: string.
  • The signature was valid, but the payload is not valid JSON or lacks a required field such as MessageSid.
  • Respond with 400, log the provider and request, and report it if it looks like a new provider format.

Types

SmsErrorCode, SerializedSmsError, HandoffUnknownReason, WebhookFailureReason, RejectionCategory, UnknownReason, and ProviderErrorInfo are exported as types from @opencoredev/sms-sdk. WebhookSignatureError, WebhookPayloadError, and WebhookFailureReason are also exported from @opencoredev/sms-sdk/webhooks.