Skip to Content
Error Handling

Error Handling

Every failure in zaileys is a typed Error subclass with a stable .code string, so you can catch problems precisely instead of parsing message text. This page documents the full error class hierarchy, every code each class can carry, how to catch and narrow them, the client error event, and how to react to connection disconnects.

The error model at a glance

zaileys ships five concrete error classes. They are all plain Error subclasses (so a generic catch (err) still works), and each one adds two extra fields:

  • code — a stable string literal you switch on to react to a specific failure.
  • cause — the underlying error that triggered this one, when there is one (set via the standard { cause } option). Always optional.

Every class is exported from the package root, so you can import them directly:

import { Client, ZaileysBuilderError, ZaileysCommandError, ZaileysDomainError, ZaileysAutomationError, ZaileysStoreError, } from 'zaileys'
ClassnameThrown byTypical trigger
ZaileysBuilderError'ZaileysBuilderError'The message builder (client.send(...), client.edit(...), content methods, album/broadcast send)Invalid content options, media load failure, send rejected by the socket
ZaileysCommandError'ZaileysCommandError'The command system (registry, middleware, dispatcher)Duplicate command, bad command name, handler/middleware threw
ZaileysDomainError'ZaileysDomainError'Domain modules (client.group, client.privacy, client.newsletter, client.community)Client not connected, group/newsletter not found, operation failed
ZaileysAutomationError'ZaileysAutomationError'Automation features (client.broadcast, client.scheduleAt, presence, rate limiter)Not connected, invalid schedule/rate config, presence update failed
ZaileysStoreError'ZaileysStoreError'Auth stores and message stores (file/sqlite/postgres/redis/convex adapters)Store unavailable, connection/read/write failure, corrupted or closed store

All five classes have an identical shape: code, message, and optional cause. Once you know how to handle one, you know how to handle them all.

Catching and narrowing

The reliable way to react to a specific failure is instanceof to pick the class, then a switch on .code to pick the case. Because code is a string literal union in TypeScript, the editor autocompletes every valid value and warns on typos.

import { Client, ZaileysBuilderError } from 'zaileys' const client = new Client() client.on('connect', async () => { try { await client.send('6281234567890@s.whatsapp.net').text('Hello!') } catch (err) { if (err instanceof ZaileysBuilderError) { switch (err.code) { case 'EMPTY_CONTENT': console.error('Nothing to send — set content before awaiting.') break case 'INVALID_RECIPIENT': console.error('Bad JID:', err.message) break case 'SEND_FAILED': console.error('WhatsApp rejected the send:', err.cause) break default: console.error('Builder error:', err.code, err.message) } } else { throw err } } })

Inspecting the cause chain

When zaileys wraps a lower-level failure (a network error, a database driver error, a Baileys socket rejection), it preserves it on .cause. Read it for the real root reason.

import { ZaileysStoreError } from 'zaileys' try { await someStoreOperation() } catch (err) { if (err instanceof ZaileysStoreError) { console.error('Store failed:', err.code) console.error('Underlying cause:', err.cause) // e.g. the pg/redis driver error } }
⚠️

cause is typed as unknown because the wrapped value can be anything. Narrow it (err.cause instanceof Error) before reading .message off it.

ZaileysBuilderError

Thrown while constructing or sending a message: content validation, media loading, and the actual socket send/relay. This is the error you will hit most often.

codeMeaning
MEDIA_LOAD_FAILEDA media source could not be fetched/read or converted — e.g. an image URL returned a non-2xx status, a local file path failed to read, or audio/sticker transcoding failed.
INVALID_RECIPIENTThe target JID is not a valid WhatsApp recipient.
USERNAME_NOT_FOUNDA @username could not be resolved to a JID.
EMPTY_CONTENTA content method was called with empty input, or you awaited the builder before setting any content (text(''), poll() with no question, no content set).
INVALID_OPTIONSOptions failed validation — out-of-range poll/album/button/list counts, invalid latitude/longitude, bad vcard, invalid mentions JID, non-positive disappearing duration, missing remoteJid, unsupported interactive socket, or client not connected.
SEND_FAILEDThe socket accepted the request but rejected or returned no message key — sendMessage/relayMessage rejected, interactive media upload failed, album child/parent send failed.
MESSAGE_NOT_FOUNDA message referenced for forwarding was not present in the store.
import { Client, ZaileysBuilderError } from 'zaileys' const client = new Client() client.on('connect', async () => { try { await client .send('6281234567890@s.whatsapp.net') .poll('Lunch?', { options: ['Pizza'] }) // only one option → INVALID_OPTIONS } catch (err) { if (err instanceof ZaileysBuilderError && err.code === 'INVALID_OPTIONS') { console.error('Fix your poll:', err.message) } } })

Note the guard at client.send(...): if you call it before the client is connected, you get a ZaileysBuilderError with code INVALID_OPTIONS and message "client not connected". See Validating before you send.

ZaileysCommandError

Thrown by the command system — registration, middleware execution, and handler dispatch.

codeMeaning
DUPLICATE_COMMANDA command key is registered more than once.
INVALID_COMMAND_NAMEA command spec is empty or contains an empty segment.
HANDLER_ERRORA command handler threw; the original error is on .cause.
MIDDLEWARE_ERRORA middleware threw, or called next() more than once.
NO_SENT_MESSAGEctx.edit(...) was used without a prior ctx.reply(...) to edit.
NOT_CONNECTEDA command operation required an active socket but the client was not connected.
import { ZaileysCommandError } from 'zaileys' try { await dispatchSomeCommand() } catch (err) { if (err instanceof ZaileysCommandError) { if (err.code === 'HANDLER_ERROR') { console.error('Command handler crashed:', err.cause) } else { console.error('Command system error:', err.code, err.message) } } }

ZaileysDomainError

Thrown by the domain modules: client.group, client.privacy, client.newsletter, and client.community.

codeMeaning
NOT_CONNECTEDThe module needs a live socket but the client is not connected.
GROUP_NOT_FOUNDThe referenced group does not exist or is not accessible.
NEWSLETTER_NOT_FOUNDThe referenced newsletter/channel could not be found.
INVALID_PARTICIPANTA participant JID was invalid for the operation.
OPERATION_FAILEDThe domain operation failed (e.g. invite code unavailable, invite acceptance failed).
import { Client, ZaileysDomainError } from 'zaileys' const client = new Client() client.on('connect', async () => { try { await client.group.acceptInvite('some-invite-code') } catch (err) { if (err instanceof ZaileysDomainError) { switch (err.code) { case 'NOT_CONNECTED': console.error('Wait for the connect event first.') break case 'OPERATION_FAILED': console.error('Invite could not be accepted:', err.message) break default: console.error('Domain error:', err.code) } } } })

ZaileysAutomationError

Thrown by automation helpers: client.broadcast, client.scheduleAt, presence updates, and the rate limiter.

codeMeaning
NOT_CONNECTEDPresence/automation needs a live socket but the client is not connected.
RATE_LIMIT_INVALIDA rate-limiter value is invalid — perSec, perJidPerSec, or burst must be greater than zero.
TASK_FAILEDA scheduled/automation task failed during execution.
SCHEDULE_INVALIDscheduleAt got a non-Date, the scheduled builder threw, or it produced no content.
STORE_UNAVAILABLEA store required by an automation feature was unavailable.
PRESENCE_FAILEDA presence update (typing, recording, etc.) failed; the cause is on .cause.
import { Client, ZaileysAutomationError } from 'zaileys' const client = new Client() client.on('connect', async () => { try { await client.scheduleAt( new Date(Date.now() + 60_000), (b) => b.text('Reminder: standup in 1 minute.'), ) } catch (err) { if (err instanceof ZaileysAutomationError && err.code === 'SCHEDULE_INVALID') { console.error('Bad schedule:', err.message) } } })

ZaileysStoreError

Thrown by storage adapters — both the auth store (session credentials) and the message store, across the file, sqlite, postgres, redis, and convex backends.

codeMeaning
STORE_NOT_AVAILABLEA required peer dependency or backend is missing (e.g. the convex package is not installed, pg.Pool constructor not found).
STORE_CONNECTION_FAILEDCould not connect to or initialize the backend — bad/conflicting config, failed schema migration, module load failure.
STORE_WRITE_FAILEDA write/delete/serialize operation against the store failed.
STORE_READ_FAILEDA read operation against the store failed.
STORE_CORRUPTEDStored data could not be parsed (e.g. a corrupted sqlite blob).
STORE_CLOSEDAn operation was attempted after the store was closed.
import { ZaileysStoreError } from 'zaileys' try { await startClientWithDatabaseStore() } catch (err) { if (err instanceof ZaileysStoreError) { switch (err.code) { case 'STORE_NOT_AVAILABLE': console.error('Missing dependency:', err.message) // e.g. "Run: pnpm add convex" break case 'STORE_CONNECTION_FAILED': console.error('Cannot reach the database:', err.cause) break case 'STORE_CORRUPTED': console.error('Session data is corrupt — clear it and re-authenticate.') break default: console.error('Store error:', err.code) } } }
⚠️

STORE_NOT_AVAILABLE for optional backends (convex, postgres pg, redis) means the peer dependency is not installed. Install it before using that adapter — see Storage Adapters.

The error event

Failures that happen outside an await you control — most importantly auto-connect failures — surface through the client’s error event instead of a thrown exception. The payload is { sessionId: string; error: Error }.

import { Client } from 'zaileys' const client = new Client() client.on('error', ({ sessionId, error }) => { console.error(`[${sessionId}] background error:`, error.message) })
⚠️

The auto-connect path only emits error if there is at least one error listener attached. If you rely on autoConnect (the default), register an error listener early so background connection failures are not swallowed. See Client.

Connection disconnects

When the underlying connection drops, the client emits a disconnect event. The payload tells you both the normalized reason and whether zaileys will automatically reconnect:

disconnect: { sessionId: string; reason: DisconnectReasonDomain; willReconnect: boolean }

reason is one of these normalized values (mapped from the raw Baileys disconnect codes):

reasonFatal?Auto-reconnectMeaning
logged-outYesNoThe session was logged out from the phone. Auth is cleared; you must re-authenticate.
connection-replacedYesNoAnother session replaced this one (same account opened elsewhere). Auth is cleared.
forbiddenYesNoThe account is blocked/forbidden by WhatsApp. Auth is cleared.
restart-requiredNoYesWhatsApp asked for a restart of the connection.
bad-sessionNoYesThe session data was bad; auth is cleared but a reconnect is attempted.
connection-closedNoYesThe connection was closed; reconnect is attempted.
connection-lostNoYesThe connection was lost (network); reconnect is attempted.
multi-device-mismatchNoYesA multi-device mismatch occurred; reconnect is attempted.
unavailable-serviceNoYesThe service was temporarily unavailable; reconnect is attempted.
unknownNoYesThe disconnect code was not recognized; reconnect is attempted.

A reason is fatal when it is logged-out, connection-replaced, or forbidden. Fatal disconnects stop the reconnect loop (willReconnect will be false); everything else is retried automatically.

import { Client } from 'zaileys' const client = new Client() client.on('disconnect', ({ reason, willReconnect }) => { if (!willReconnect) { // Fatal: logged-out / connection-replaced / forbidden console.error(`Connection ended permanently (${reason}). Re-authentication required.`) // Surface a fresh QR/pairing flow, alert an operator, etc. return } console.warn(`Disconnected (${reason}); reconnecting automatically...`) }) client.on('reconnecting', ({ attempt, delayMs, reason }) => { console.log(`Reconnect attempt ${attempt} in ${delayMs}ms (reason: ${reason})`) })

The bad-session, connection-replaced, forbidden, and logged-out reasons clear stored auth credentials. Of those, only bad-session triggers a reconnect — the other three are fatal. For the full lifecycle, see Events.

Best-practice patterns

Validating before you send

The cheapest error is the one you never trigger. Guard against the two most common preventable failures — sending before connect, and empty/invalid content — at the call site.

import { Client, ZaileysBuilderError } from 'zaileys' const client = new Client() async function safeSend(jid: string, text: string) { if (client.state !== 'connected') { // Avoids INVALID_OPTIONS "client not connected" throw new Error('Not connected yet — wait for the connect event.') } if (!text.trim()) { // Avoids EMPTY_CONTENT return } try { return await client.send(jid).text(text) } catch (err) { if (err instanceof ZaileysBuilderError && err.code === 'SEND_FAILED') { console.error('Delivery failed for', jid, '-', err.cause) return } throw err } }

Handling send failures without crashing

A single failed send should never take down a long-running bot. Wrap each outbound send and decide locally whether to retry, skip, or alert.

import { Client, ZaileysBuilderError } from 'zaileys' const client = new Client() async function sendWithRetry(jid: string, text: string, attempts = 3) { for (let i = 1; i <= attempts; i++) { try { return await client.send(jid).text(text) } catch (err) { const isTransient = err instanceof ZaileysBuilderError && err.code === 'SEND_FAILED' if (!isTransient || i === attempts) throw err await new Promise((r) => setTimeout(r, 500 * i)) } } }

Per-recipient broadcast errors

client.broadcast(...) never rejects on a single bad recipient. Instead it resolves to a BroadcastResult that partitions outcomes — sent is an array of JIDs that succeeded, failed is an array of { jid, error } for the ones that did not. Always inspect failed.

import { Client } from 'zaileys' const client = new Client() const recipients = [ '6281111111111@s.whatsapp.net', '6282222222222@s.whatsapp.net', '6283333333333@s.whatsapp.net', ] client.on('connect', async () => { const result = await client.broadcast( recipients, (b) => b.text('Scheduled maintenance tonight at 22:00.'), { rateLimitPerSec: 5, onProgress: (done, total, jid, ok) => { console.log(`[${done}/${total}] ${jid} ${ok ? 'sent' : 'failed'}`) }, }, ) console.log(`Sent ${result.sent.length}, failed ${result.failed.length}`) for (const { jid, error } of result.failed) { console.error(`Could not reach ${jid}:`, error.message) } })

onProgress fires once per recipient with (done, total, jid, ok), so you can stream progress live and still get the full partitioned result at the end.

A single top-level handler

For everything else, a small helper that narrows across all five classes keeps your call sites clean.

import { ZaileysBuilderError, ZaileysCommandError, ZaileysDomainError, ZaileysAutomationError, ZaileysStoreError, } from 'zaileys' function describeError(err: unknown): string { if ( err instanceof ZaileysBuilderError || err instanceof ZaileysCommandError || err instanceof ZaileysDomainError || err instanceof ZaileysAutomationError || err instanceof ZaileysStoreError ) { return `${err.name} [${err.code}]: ${err.message}` } return err instanceof Error ? err.message : String(err) }

Never let error events go unobserved

import { Client } from 'zaileys' const client = new Client() // Register before connecting so auto-connect failures are reported. client.on('error', ({ error }) => { console.error('Client error:', error.message) })
🚫

On Node.js, an EventEmitter that emits error with no listener throws. zaileys guards the auto-connect path by only emitting error when a listener exists — but you should still attach one so failures are visible rather than silent.

Quick reference

ClassCodes
ZaileysBuilderErrorMEDIA_LOAD_FAILED, INVALID_RECIPIENT, USERNAME_NOT_FOUND, EMPTY_CONTENT, INVALID_OPTIONS, SEND_FAILED, MESSAGE_NOT_FOUND
ZaileysCommandErrorDUPLICATE_COMMAND, INVALID_COMMAND_NAME, HANDLER_ERROR, MIDDLEWARE_ERROR, NO_SENT_MESSAGE, NOT_CONNECTED
ZaileysDomainErrorNOT_CONNECTED, GROUP_NOT_FOUND, NEWSLETTER_NOT_FOUND, INVALID_PARTICIPANT, OPERATION_FAILED
ZaileysAutomationErrorNOT_CONNECTED, RATE_LIMIT_INVALID, TASK_FAILED, SCHEDULE_INVALID, STORE_UNAVAILABLE, PRESENCE_FAILED
ZaileysStoreErrorSTORE_NOT_AVAILABLE, STORE_CONNECTION_FAILED, STORE_WRITE_FAILED, STORE_READ_FAILED, STORE_CORRUPTED, STORE_CLOSED
  • Clientconnect, autoConnect, lifecycle, and the error/disconnect events.
  • Sending Messages — the builder API that produces ZaileysBuilderError.
  • Troubleshooting — diagnosing common real-world failures.
Last updated on