zeroclaw-labs/zeroclaw · error

WhatsApp Web client not connected. Initialize the bot first.

Error message

WhatsApp Web client not connected. Initialize the bot first.

What it means

The WhatsApp Web channel stores its client behind a lock; it is `Some` only between a successful `listen()` handshake and shutdown. `send` bails with this message when the client is `None` — the bot was never initialized, is still connecting (QR not yet scanned), or has disconnected and is inside the reconnect cycle.

Source

Thrown at crates/zeroclaw-channels/src/whatsapp_web.rs:2417

            ::zeroclaw_api::attribution::ChannelKind::WhatsappWeb,
        )
    }
    fn alias(&self) -> &str {
        &self.alias
    }
}

#[cfg(feature = "whatsapp-web")]
#[async_trait]
impl Channel for WhatsAppWebChannel {
    fn name(&self) -> &str {
        "whatsapp"
    }

    async fn send(&self, message: &SendMessage) -> Result<()> {
        let client = self.client.lock().clone();
        let Some(client) = client else {
            anyhow::bail!("WhatsApp Web client not connected. Initialize the bot first.");
        };

        // Validate recipient allowlist only for direct phone-number targets.
        if !Self::is_jid(&message.recipient) {
            let normalized = self.normalize_phone(&message.recipient);
            if !self.is_number_allowed(&normalized) {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
                    &format!("recipient {} not in allowed list", message.recipient)
                );
                return Ok(());
            }
        }

        let deliverable_recipient = Self::resolve_outbound_recipient(&message.recipient);
        let to = self.recipient_to_jid(&deliverable_recipient)?;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Wait for channel readiness (health/connectivity signal or the first inbound message) before queueing outbound messages.
  2. Ensure `listen()` is running; if it exited, restart the channel (and re-pair if the session was purged).
  3. Retry the send after a delay to ride out the reconnect window.

Example fix

// before: fire immediately at process start
channel.send(&greeting).await?; // may race QR pairing

// after: wait for the client to be connected
while !channel.health_check().await {
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
channel.send(&greeting).await?;
Defensive patterns

Strategy: retry

Validate before calling

// Wait until the WhatsApp Web client is live before sending
while !channel.health_check().await {
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
channel.send(&msg).await

Type guard

async fn web_client_connected(channel: &WhatsAppWebChannel) -> bool {
    channel.health_check().await
}

Try / catch

match channel.send(&msg).await {
    Err(e) if e.to_string().contains("not connected") => {
        // reconnect window: wait for the client, then retry once
        while !channel.health_check().await {
            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
        }
        channel.send(&msg).await
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling `send` before `listen()` completed pairing; sending while the channel is between a disconnect and its next reconnect attempt; sending on a channel whose `listen()` task has exited or given up.

Common situations: Startup races — an outbound greeting or approval request firing before the QR was scanned; sends landing in the reconnect window (backoff up to 300s between attempts); using a channel handle after shutdown.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/5d9461252335c38c. Report an issue: GitHub.