docs

Ease docs/Concepts/Errors

Errors

Every failure the web SDK raises is an EaseError carrying a stable code string and a human-readable message. Catch the type, branch on the code, and reach for the extra payload fields on the few codes that ship one. Network-level failures are the exception. They reach you as the platform's native error, not an EaseError.

The shape of an error

Every failure the SDK raises is an EaseError. It carries:

  • code: a stable string that uniquely identifies the failure (not_connected, invalid_room_id, duplicate_roster). This is the field to branch on.
  • message: a human-readable sentence. Useful in logs, not stable across versions. Do not switch on it.
  • httpStatus: the HTTP status when the error came from a non-2xx response; undefined for failures that never touched the wire (local validation, a libsignal decrypt failure).
  • existingGroupId: set only on duplicate_roster: the id of the pre-existing group with the same roster.
  • pendingRequestId: set only on pending_approval: the id of the queued nomination, for correlating the eventual resolution event.

Catching

The shape is the same everywhere: narrow to EaseError, then read code. Re-throw anything you do not handle so you never swallow an error you did not expect.

Branch on a single code

When one code carries a payload you act on, check it directly and use the extra field.

TypeScript · Web SDK
try {
  await ease.chat.createGroup('Crew', [bobId, carolId]);
} catch (err) {
  if (err instanceof EaseError && err.code === 'duplicate_roster') {
    // err.existingGroupId holds the id of the matching group.
    // Offer "open existing" or retry with { force: true }.
  } else {
    throw err;
  }
}

Switch across a set of codes

When a method can fail several ways, narrow the type once and switch.

TypeScript · Web SDK
try {
  await ease.chat.group(groupId).promote(carolUserId);
} catch (err) {
  if (!(err instanceof EaseError)) throw err;
  switch (err.code) {
    case 'forbidden':          /* show "owner or admin only" */ break;
    case 'admin_cap_reached':  /* show "at most three admins" */ break;
    case 'already_admin':      /* no-op, already promoted */ break;
    case 'member_not_present': /* show "not a member" */ break;
    default: throw err;
  }
}

Auth and session

Raised by ease.connect(), the sign-up and login flows, and any method that needs a live session.

  • not_connected: a method that needs the tenant was called before ease.connect() resolved. Connect first.
  • not_signed_in: a session-bearing method was called before sign-up, login, or session restore. Authenticate first.
  • credentials_invalid: wrong email and password combination. Phone or method mismatches also collapse to this code so the server cannot be used to enumerate accounts.
  • auth_method_mismatch: the call used a credential the tenant is not configured for (email against a phone tenant, or the reverse).
  • tenant_email_not_configured: the tenant has no OTP delivery provider yet, so signup codes cannot be sent. Connect an email (Resend) or SMS (Twilio) provider in the dashboard; this is tenant configuration, not something the SDK call can retry around.
  • otp_invalid: the one-time code was missing, expired, or wrong. Send a fresh code and retry.
  • email_invalid / phone_invalid: the address or number failed shape validation before any request was sent.
  • session_already_running: a session is already active; tear it down before starting another.
  • refresh_failed: the SDK could not refresh the access token. Re-authenticate.
  • api_key_missing: no API key reached the bootstrap call. Construct the client with your tenant key.
  • api_key_malformed: the key is not a valid key shape. Copy it again from the dashboard.
  • api_key_invalid: the key does not match a tenant. Copy it again from the dashboard.
  • api_key_revoked: the key was revoked in the dashboard. Issue a new one and ship it.
  • api_key_env_mismatch: a key for one environment was used against another. Check which key the build embeds.
  • phone_invalid: the phone number failed validation. Normalize to E.164 before calling.
  • otp_expired: the one-time code aged out. Send a fresh one.
  • otp_attempts_exceeded: too many wrong tries against one code. Send a fresh code; the counter resets.
  • otp_rate_limited: codes were requested too quickly. Wait before requesting another; surface a cooldown in the UI.
  • tenant_sms_not_configured: the tenant has no SMS provider connected, so phone codes cannot be sent. Connect Twilio in the dashboard; not retryable from the SDK.
  • email_send_failed: the tenant's email provider rejected the send. Check the provider credentials and sending domain in the dashboard.
  • sms_send_failed: the tenant's SMS provider rejected the send. Check the Twilio configuration in the dashboard.
  • password_weak: the password failed the strength policy. Ask for a longer one.
  • email_taken: an account already exists for this email. Offer login instead.
  • username_taken: the username is in use. Offer another.
  • username_invalid: the username failed validation (3–30 chars, allowed characters). Show the rule inline.
  • name_required: the profile name was empty at sign-up. Collect it.
  • refresh_token_invalid: the saved session can no longer be refreshed (revoked, expired, or device removed). Clear it and send the user through login.

Passkeys

Raised by the passkey registration, login, and secret-derivation paths. Most reflect a browser or device limitation rather than a server response.

  • passkey_unsupported: the browser has no WebAuthn platform authenticator. Fall back to password auth.
  • passkey_prf_unavailable: the browser or the existing passkey lacks the WebAuthn PRF extension the SDK needs to derive the keystore key. A passkey account cannot be created or unlocked here.
  • passkey_cancelled: the prompt was dismissed, timed out, or returned no credential. The user can retry.
  • passkey_already_registered: a passkey for this account already exists on this device.
  • passkey_audience_reserved: the audience passed to secret derivation is reserved for the SDK. Choose a different audience string.
  • passkey_failed: the WebAuthn call failed for another reason; the underlying error name is in the message.
  • bad_passphrase: the password or passkey did not unlock the local libsignal keystore.

Contacts

Raised by lookups, contact requests, and the welcome-message path.

  • not_found: a lookup matched no registered user. The handle is unclaimed or not discoverable.
  • self_contact_invalid: a contact request was addressed to the caller's own account.
  • welcome_too_long: the welcome message exceeded the per-request character or byte cap. Shorten it.
  • welcome_empty: a welcome message was required but empty.
  • welcome_not_decrypted: accept was called before the incoming request list was fetched, so the SDK has no plaintext to re-encrypt. List incoming requests first, then accept.
  • invalid_handle: the email, phone, or username handle failed validation server-side. Validate before sending.
  • lookup_kind_not_supported_by_tenant: the tenant does not support looking up users by this handle kind (for example phone lookup on an email tenant). Use the tenant's auth handle kind.
  • user_not_found: no user matches the handle or id. Surface not-found; do not retry.
  • already_contacts: you are already mutual contacts. Open the DM instead.
  • already_blocked: a block exists between the two users. The request cannot be sent.
  • too_many_pending_requests: the outgoing-request cap was hit. Wait for responses or withdraw some.
  • request_not_found: the request id no longer exists (withdrawn or already handled). Refresh the request list.
  • not_recipient: only the recipient of a request can accept it. Refresh state; this is a wrong-side call.
  • rate_limited: too many lookups or requests in a window. Back off and retry after a pause.

Chat and rooms

Raised by the room factories, the send path, and the group roster methods. The roster codes (forbidden and the promote/demote/transfer set) reach you from the server as an EaseError; the SDK documents them on the methods that raise them.

  • invalid_room_id: the id passed to a room factory has the wrong prefix for that room type (a group id where a DM id was expected, and so on).
  • message_too_large: the plaintext exceeded the per-message cap. Split the payload or move large content to a blob.
  • name_empty / name_too_long: a group or profile name was blank or over the length cap.
  • duplicate_roster: createGroup matched an existing group with the same members. Read existingGroupId off the error to open it, or retry with { force: true } to mint a fresh one.
  • pending_approval: adding a member to an approval-gated group queued a nomination rather than completing it. pendingRequestId correlates the later resolution event.
  • creator_must_transfer_first: the owner tried to leave a group that still has members. Transfer ownership or disband first.
  • forbidden: the caller's role does not permit the action (a non-owner attempting an owner-only verb, for example).
  • member_not_present: the target of a promote, demote, or transfer is not a current member.
  • already_admin / not_admin: promote on someone already an admin or owner, or demote on someone who is not an admin.
  • already_owner: transferOwnership targeted the current owner.
  • admin_cap_reached: the promotion would exceed the group's admin cap.
  • group_not_found: the group id does not exist or you are not a member. Refresh the group list.
  • group_too_small: a group needs at least the creator plus one member. Add a member.
  • group_full: the member cap was reached. Not retryable without removing someone.
  • member_set_invalid: the member list contains duplicates or malformed ids. Fix the list.
  • member_not_in_contacts: every founding or added member must be in your contacts first. Send the contact request, then add.
  • member_already_present: the user is already in the group. Refresh members.
  • nomination_already_pending: an add for this user is already awaiting approval. Nothing to do.
  • name_too_long: the group name exceeds the cap. Shorten it.
  • invite_token_invalid: the join token is wrong, rotated away, or expired. Ask for a fresh QR or link.
  • qr_join_disabled: the group has QR and link joining switched off. Join by member add instead.
  • member_muted: you are muted in this group until the mute passes. Sends are rejected; receiving still works.
  • cannot_mute_owner: the group owner can never be muted. Owner-proofing, not a transient failure.

Crypto and identity

Raised by the libsignal-backed send path and the self-device attestation check. These never touch the wire, so httpStatus is undefined.

  • recipient_identity_changed: the recipient's identity key changed since this device last messaged them; they likely re-registered or signed in on a fresh device. Surface the change and let the user re-accept the contact before retrying.
  • recipient_no_session: no usable libsignal session for the recipient on this device. Their key bundle may be incomplete or this device's session is stale.
  • recipient_unmessageable: libsignal could not encrypt to the recipient for another protocol reason.
  • no_local_identity: the local libsignal store is unavailable. Sign in (or, on web, log in with a password) to rehydrate it before messaging.
  • identity_key_mismatch: the local identity key does not match the server-stored value for this device.
  • ciphertext_invalid: a self-sealed blob was too short or otherwise malformed to decode.
  • identity_missing: the local encryption identity is absent (fresh restore on web, or pre-login). A fresh login with password recreates it.

Blobs and media

Raised by blob upload, download, and decrypt.

  • blob_ref_invalid: a BlobRef was missing its content key or nonce, or the key or nonce was the wrong length.
  • blob_decrypt_failed: AES-GCM decryption failed: a tag mismatch, a wrong key, or a truncated ciphertext.
  • blob_upload_failed / blob_download_failed: the upload or download request returned a non-2xx status. The server's own code overrides this default when it supplies one; httpStatus holds the status.

Socket

Raised by the realtime socket and by request-response calls that ride it.

  • socket_open_failed / socket_error: the socket could not be opened. The SDK reconnects on its own; surface a transient banner rather than a hard failure.
  • socket_closed: a socket method was called before the socket was open, before the hello handshake completed, or after the socket closed. Open the socket and retry.
  • socket_protocol_error: the server sent a frame the SDK could not parse or did not expect.

Server-propagated and unmapped codes

When a request fails and the server returns a code the SDK does not special-case, the SDK surfaces it verbatim as the EaseError code, with the HTTP status on httpStatus. If the server returns no code, the SDK synthesises http_<status> (for example http_500) so you always have something stable to branch on. Internal server failures over the socket surface as server_error.

Network failures are not EaseError

A transport-level failure (the request never reaching the server, a dropped connection, a DNS failure) is not wrapped. On the web it reaches you as the native fetch rejection, a TypeError, not an EaseError. Branch on it before assuming every caught value is an EaseError:

  • instanceof EaseError is true. Branch on code as above.
  • instanceof EaseError is false. Treat it as a network or environment failure: retry with backoff, or surface an offline state.

Still stuck

If an error does not match anything on this page, or the recovery move does not work, write to support@securegroupchat.com with the error code, the SDK version, and what you called. During the closed beta that inbox is read by the people who built the SDK.

See also