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
- Wait for channel readiness (health/connectivity signal or the first inbound message) before queueing outbound messages.
- Ensure `listen()` is running; if it exited, restart the channel (and re-pair if the session was purged).
- 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
- Queue outbound messages until the first inbound event or a positive health check confirms pairing.
- Keep `listen()` supervised; a dead listen task makes every send fail this way.
- Expect reconnect backoff up to 300s — size send-retry timeouts accordingly.
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
- QR payload is empty
- Recipient cannot be empty
- Recipient `{trimmed}` does not contain a valid phone number
- TTS returned empty audio
- WhatsApp marker target {} is empty
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/5d9461252335c38c.
Report an issue: GitHub.