Skip to main content
As of PR #893, all feature-domain APIs return a typed, domain-specific error instead of anyhow::Error. Use ? to propagate errors into any anyhow context — all types implement std::error::Error and are #[non_exhaustive], so your existing error-handling code compiles unchanged. Lower-level APIs such as Client::connect and media upload/download are not covered by this page and may still surface anyhow::Error directly.

Error hierarchy

ClientError          (transport/connection base — embedded by select domain errors)
  ├── NotConnected
  ├── Socket(SocketError)
  ├── EncryptSend(EncryptSendError)
  ├── AlreadyConnected
  ├── NotLoggedIn
  ├── Iq(IqError)
  └── Internal(anyhow::Error)

IqError              (IQ request failures — embedded by most domain errors)
  ├── Timeout
  ├── NotConnected
  ├── Socket(SocketError)
  ├── EncryptSend(EncryptSendError)
  ├── ClientState(Box<ClientError>)
  ├── Disconnected
  ├── ServerError
  ├── UnexpectedResponseType
  ├── InternalChannelClosed
  ├── EncodeError
  └── ParseError
Domain errors that embed IqError or ClientError via #[from] propagate those failures automatically via ?. Some errors (e.g. AppStateError, SignalError) use internal anyhow::Error wrapping instead and do not have Iq or Client variants.

Domain error types

Error typeReturned byModule
SendErrorsend_message, revoke_message, edit_message, pin_message, send_reaction, and all other send-path methodswhatsapp_rust
GroupErrorAll Groups::* methodswhatsapp_rust
BlockingErrorAll Blocking::* methodswhatsapp_rust
AppStateErrorAll ChatActions::* and Labels::* methodswhatsapp_rust
ChatStateErrorAll Chatstate::send* methodswhatsapp_rust
CommunityErrorAll Community::* methodswhatsapp_rust
ContactErrorAll Contacts::* methodswhatsapp_rust
NewsletterErrorAll Newsletter::* methodswhatsapp_rust
PollErrorPolls::{create, create_quiz, vote, decrypt_vote, aggregate_votes}whatsapp_rust
ProfileErrorAll Profile::* methodswhatsapp_rust
SignalErrorAll Signal::* methodswhatsapp_rust
TcTokenErrorAll TcToken::* methodswhatsapp_rust
MediaReuploadErrorMediaReupload::requestwhatsapp_rust
PresenceErrorAll Presence::* methodswhatsapp_rust

Type definitions

SendError

#[non_exhaustive]
pub enum SendError {
    #[error(transparent)]
    Client(ClientError),
    #[error("client is not logged in")]
    NotLoggedIn,
    #[error("IQ request failed: {0}")]
    Iq(#[from] IqError),
    #[error("invalid send request: {0}")]
    InvalidRequest(String),
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}

GroupError

#[non_exhaustive]
pub enum GroupError {
    #[error(transparent)]
    Iq(#[from] IqError),
    #[error(transparent)]
    Mex(#[from] MexError),
    #[error("invalid group request: {0}")]
    InvalidRequest(String),
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}

BlockingError

#[non_exhaustive]
pub enum BlockingError {
    #[error(transparent)]
    Iq(#[from] IqError),
    #[error("invalid blocklist target: {0}")]
    InvalidJid(String),
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}

AppStateError

Shared by ChatActions and Labels.
#[non_exhaustive]
pub enum AppStateError {
    #[error("invalid app-state request: {0}")]
    InvalidRequest(String),
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}

ChatStateError

#[non_exhaustive]
pub enum ChatStateError {
    #[error(transparent)]
    Client(#[from] ClientError),
}

CommunityError

#[non_exhaustive]
pub enum CommunityError {
    #[error(transparent)]
    Iq(#[from] IqError),
    #[error(transparent)]
    Mex(#[from] MexError),
    #[error(transparent)]
    Group(#[from] GroupError),
    #[error("invalid community request: {0}")]
    InvalidRequest(String),
}

ContactError

#[non_exhaustive]
pub enum ContactError {
    #[error(transparent)]
    Iq(#[from] IqError),
    #[error("unsupported contact JID: {0}")]
    InvalidJid(String),
}

NewsletterError

#[non_exhaustive]
pub enum NewsletterError {
    #[error(transparent)]
    Mex(#[from] MexError),
    #[error(transparent)]
    Iq(#[from] IqError),
    #[error(transparent)]
    Client(#[from] ClientError),
    #[error("invalid newsletter request: {0}")]
    InvalidRequest(String),
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}

PollError

#[non_exhaustive]
pub enum PollError {
    #[error(transparent)]
    Send(#[from] SendError),
    #[error("invalid poll: {0}")]
    InvalidPoll(String),
    #[error("client is not logged in")]
    NotLoggedIn,
    #[error("poll vote crypto failed: {0}")]
    Crypto(#[source] anyhow::Error),
}

ProfileError

#[non_exhaustive]
pub enum ProfileError {
    #[error(transparent)]
    Iq(#[from] IqError),
    #[error(transparent)]
    Client(#[from] ClientError),
    #[error("invalid argument: {0}")]
    InvalidArgument(String),
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}

SignalError

#[non_exhaustive]
pub enum SignalError {
    #[error(transparent)]
    Protocol(#[from] SignalProtocolError),
    #[error("unsupported signal operation: {0}")]
    Unsupported(String),
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}

TcTokenError

#[non_exhaustive]
pub enum TcTokenError {
    #[error(transparent)]
    Iq(#[from] IqError),
    #[error(transparent)]
    Store(#[from] StoreError),
}

MediaReuploadError

#[non_exhaustive]
pub enum MediaReuploadError {
    #[error(transparent)]
    Client(#[from] ClientError),
    #[error("client is not logged in")]
    NotLoggedIn,
    #[error("invalid media reupload request: {0}")]
    InvalidRequest(String),
    #[error("media retry notification timed out")]
    Timeout,
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}

PresenceError

#[non_exhaustive]
pub enum PresenceError {
    #[error("cannot send presence without a push name set")]
    PushNameEmpty,
    #[error(transparent)]
    Client(#[from] ClientError),
    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

ClientError (base type)

#[non_exhaustive]
pub enum ClientError {
    #[error("client is not connected")]
    NotConnected,
    #[error("client is already connected")]
    AlreadyConnected,
    #[error("client is not logged in")]
    NotLoggedIn,
    #[error("socket error: {0}")]
    Socket(SocketError),
    #[error("encrypt/send error: {0}")]
    EncryptSend(EncryptSendError),
    #[error("IQ request failed: {0}")]
    Iq(#[from] IqError),
    #[error(transparent)]
    Internal(#[from] anyhow::Error),
}

IqError (base type)

#[non_exhaustive]
pub enum IqError {
    #[error("IQ request timed out")]
    Timeout,
    #[error("client is not connected")]
    NotConnected,
    #[error("socket error")]
    Socket(SocketError),
    #[error("encrypted send pipeline failed")]
    EncryptSend(EncryptSendError),
    #[error("client state prevented send")]
    ClientState(Box<ClientError>),  // boxed to break the ClientError<->IqError cycle
    #[error("received disconnect node during IQ wait")]
    Disconnected,
    #[error("received a server error response: code={code}, text='{text}'")]
    ServerError { code: u16, text: String, error_type: Option<String>, backoff: Option<u32> },
    #[error("received unexpected IQ response type")]
    UnexpectedResponseType { got: Option<String> },
    #[error("internal channel closed unexpectedly")]
    InternalChannelClosed,
    #[error("failed to encode IQ request")]
    EncodeError(anyhow::Error),
    #[error("failed to parse IQ response")]
    ParseError(anyhow::Error),
}
IqError::ClientState holds a Box<ClientError> (not ClientError directly) to break the mutual-size cycle between ClientError and IqError. Pattern matching needs dereferencing: IqError::ClientState(e) => { /* *e is a ClientError */ }.

Migration guide

From anyhow::Error

If your handler used ? into anyhow:
// Before — still compiles unchanged
async fn my_fn() -> anyhow::Result<()> {
    client.send_message(jid, msg).await?;  // SendError: Into<anyhow::Error>
    Ok(())
}
If you were matching on anyhow::Error downcasts, switch to typed matching:
// Before
match client.send_message(jid, msg).await {
    Ok(r) => { /* ... */ }
    Err(e) => eprintln!("send failed: {e}"),
}

// After — match specific variants
use whatsapp_rust::SendError;
match client.send_message(jid, msg).await {
    Ok(r) => { /* ... */ }
    Err(SendError::NotLoggedIn) => eprintln!("not authenticated"),
    Err(SendError::Iq(e)) => eprintln!("IQ failed: {e}"),
    Err(SendError::InvalidRequest(msg)) => eprintln!("bad request: {msg}"),
    Err(e) => eprintln!("send failed: {e}"),
}

IqError::ClientState boxing

// Before
Err(IqError::ClientState(e)) => { /* e: ClientError */ }

// After — dereference the box
Err(IqError::ClientState(e)) => { /* *e: ClientError */ }