> ## Documentation Index
> Fetch the complete documentation index at: https://www.cometchat.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Send While Offline

> Queue messages sent without a connection, track their status, and retry failed sends with the CometChat React Native Chat SDK.

> [accordion] AI Integration Quick Reference
>
> | Field | Value |
> | - | - |
> | Send | `CometChat.sendMessage(message, listener)` — also `sendMediaMessage`, `sendCustomMessage`, `sendInteractiveMessage` |
> | Listener | `MessageSentListener`: `onMessagePending` → `onMessageSent` or `onError(error, reason)`. Only a send with a listener is queued. |
> | Message state | `message.getLocalMessageId()`, `message.getMessageStatus()`, `message.getFailureReason()` |
> | Outbox | `getPendingMessages()`, `retryMessage(localId)`, `retryAllFailedMessages()`, `discardLocalMessage(localId)` |
> | App-wide events | `addOfflineMessageListener(id, listener)` / `removeOfflineMessageListener(id)` |
> | Prerequisites | [Offline support enabled](https://www.cometchat.com/docs/sdk/react-native/offline-enable.md) |
>
> ```typescript theme={null}
> CometChat.sendMessage(textMessage, {
>   onMessagePending: (message) => { /* getMessageStatus() === PENDING, fires immediately */ },
>   onMessageSent: (message) => { /* server ack: real id + sentAt */ },
>   onError: (error, reason) => { /* reason == null: queued, not failed. reason set: real failure */ },
> });
>
> const localId = message.getLocalMessageId();
> const status = message.getMessageStatus();     // PENDING | SENDING | SENT | DELIVERED | READ | FAILED
> const why = message.getFailureReason();        // set only while FAILED
>
> await CometChat.getPendingMessages();          // PENDING + SENDING + FAILED
> await CometChat.retryMessage(localId);
> await CometChat.retryAllFailedMessages();      // transient failures only
> await CometChat.discardLocalMessage(localId);  // delete an unsent message
> ```

With offline support enabled, a message you send while the device has no connection is **durably queued on the device** instead of failing. It keeps its place across an app restart and is sent automatically once the connection is back and stable (with `autoSendPendingOnReconnect` on, the default). This page shows the offline-aware send listener, how to read a message's status, how to inspect and recover the send queue, and the queue limits.

> [!NOTE]
>
> Offline support is **off by default** and requires SDK **4.2.0** or newer. Queuing only happens once you [enable it](https://www.cometchat.com/docs/sdk/react-native/offline-enable.md); with offline off, a send over no connection fails exactly as before.

## Send a Message

Pass a `MessageSentListener` as the second argument to `sendMessage`. Its lifecycle is normally `onMessagePending` first (the message is durably queued), then its outcome: `onMessageSent` (the server accepted it) or `onError`. Offline, `onError` is a queued-not-failed notice and `onMessageSent` still fires later on reconnect, so both can fire for one send (see below). If the message cannot be queued at all, for example because its media could not be copied (reason `PERMANENT`), only `onError` fires.

> [tab] TypeScript
>
> ```typescript theme={null}
> const textMessage: CometChat.TextMessage = new CometChat.TextMessage(
>   "UID",
>   "Hello!",
>   CometChat.RECEIVER_TYPE.USER
> );
>
> CometChat.sendMessage(textMessage, {
>   onMessagePending: (message: CometChat.BaseMessage) => {
>     // Queued and persisted; message.getMessageStatus() === CometChat.MessageStatus.PENDING
>     console.log("Pending, local id:", message.getLocalMessageId());
>   },
>   onMessageSent: (message: CometChat.BaseMessage) => {
>     // Server ack: message now carries its real id and sentAt
>     console.log("Sent:", message.getId());
>   },
>   onError: (error: CometChat.CometChatException, reason?: CometChat.MessageFailureReason | null) => {
>     // reason == null: queued, not failed. Otherwise a real failure.
>     console.log("Send error:", error.code, reason);
>   },
> });
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> const textMessage = new CometChat.TextMessage(
>   "UID",
>   "Hello!",
>   CometChat.RECEIVER_TYPE.USER
> );
>
> CometChat.sendMessage(textMessage, {
>   onMessagePending: (message) => {
>     // Queued and persisted; message.getMessageStatus() === CometChat.MessageStatus.PENDING
>     console.log("Pending, local id:", message.getLocalMessageId());
>   },
>   onMessageSent: (message) => {
>     // Server ack: message now carries its real id and sentAt
>     console.log("Sent:", message.getId());
>   },
>   onError: (error, reason) => {
>     // reason == null: queued, not failed. Otherwise a real failure.
>     console.log("Send error:", error.code, reason);
>   },
> });
> ```

The same `MessageSentListener` is accepted by all four send methods:

```typescript theme={null}
CometChat.sendMessage(textMessage, listener);                  // TextMessage
CometChat.sendMediaMessage(mediaMessage, listener);            // MediaMessage
CometChat.sendCustomMessage(customMessage, listener);          // CustomMessage
CometChat.sendInteractiveMessage(interactiveMessage, listener); // InteractiveMessage
```

> [!NOTE]
>
> Passing a listener does not change the return value — each method still returns its Promise, and the Promise settles on the server outcome. Consume the final result from **one** channel, the Promise or the listener, not both. `onMessagePending` fires only while offline support is active.
>
> Only a send **with a listener** is queued. A send with no listener goes straight to the server and is never queued, so while offline it rejects and is not sent later. If you pass a listener and do not use the Promise, add a `.catch(() => {})` to it so a rejection is not reported as unhandled.

## Queued, Not Failed

When the device is offline, the send still fires `onMessagePending`, then fires `onError` **once** with the not-connected error: code `FAILED_TO_FETCH`, `errorParams.queued === true`, and `reason` equal to `null`. The Promise rejects with the same error. This is a **notification, not a failure**: the message stays durably `PENDING`, and `onMessageSent` fires **later**, on reconnect, when it actually goes. Do not send it again. This is the default behaviour, with `autoSendPendingOnReconnect` on. With it off, an offline send becomes `FAILED` (`TRANSIENT`) right away: `onError` fires with a reason, and you recover the message with `retryMessage`.

So for a single send, `onError` and `onMessageSent` can **both** fire (offline, then reconnect), and `onError` does not always mean the message failed. Use `reason` (or `error.errorParams.queued`) to tell the two apart:

| `onError` arguments | Meaning | What to do |
| - | - | - |
| `reason == null`, `error.errorParams.queued === true` | Queued; not sent yet. Auto-sends on reconnect (with `autoSendPendingOnReconnect` on, the default). | Show as sending/queued; nothing to do. |
| `reason` is a `MessageFailureReason` | A real failure; will not retry on its own. | Offer the user a manual retry (see below). |

> [tab] TypeScript
>
> ```typescript theme={null}
> onError: (error: CometChat.CometChatException, reason?: CometChat.MessageFailureReason | null) => {
>   if (reason == null) {
>     // Queued offline — auto-sends on reconnect
>   } else {
>     // Real failure — surface a retry action
>   }
> },
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> onError: (error, reason) => {
>   if (reason == null) {
>     // Queued offline — auto-sends on reconnect
>   } else {
>     // Real failure — surface a retry action
>   }
> },
> ```

### Message status and failure reason

Every message exposes its offline state. `getLocalMessageId()` returns the SDK-generated handle that is stamped at compose (while offline is running) and stays stable across the whole lifecycle — use it to retry or discard the message.

| Accessor | Type | Notes |
| - | - | - |
| `getLocalMessageId()` | `string \| undefined` | Client handle; present while offline is running. |
| `getMessageStatus()` | `MessageStatus` | `PENDING`, `SENDING`, `SENT`, `DELIVERED`, `READ`, `FAILED`. |
| `getFailureReason()` | `MessageFailureReason \| undefined` | Set only while `FAILED`; otherwise `undefined`. |

> [!WARNING]
>
> Read the offline status with `getMessageStatus()`. `BaseMessage.getStatus()` is the existing call/message status field and is **not** the offline status.

`MessageFailureReason` values:

| Reason | When |
| - | - |
| `TRANSIENT` | A temporary server error (5xx, or 429 rate-limited), no answer after 5 automatic reconnect attempts in a row, or, with `autoSendPendingOnReconnect` off, no answer at all. Retryable. |
| `PERMANENT` | The server rejected the message, or the error was not recognized. A manual retry will not help. |
| `QUEUE_FULL` | A new send was refused because the queue is full (count cap). |
| `QUEUE_OVERFLOW` | An older queued message was evicted to admit a newer one (`DROP_OLDEST` only). |
| `BYTE_OVERFLOW` | A media send was refused because its bytes do not fit in `maxMediaCacheSize`, the budget for the whole media cache, even after cached received media is evicted to make room. |
| `DISCARDED` | A queued message was removed before it could send: by `clearOfflineCache()`, by a cache rebuild, or by leaving or deleting its conversation. Reported with code `ERROR_OFFLINE_MESSAGE_DISCARDED`. (`discardLocalMessage` reports `onSuccess` instead. A logout ends unsent sends without calling any listener.) |

## Inspect the Queue

`getPendingMessages` returns the **entire un-acked outbox** — every `PENDING`, `SENDING`, and `FAILED` message, ordered by local compose time. Sent messages are not included. Read each message's status to group them. It returns an empty list when offline support is off.

> [tab] TypeScript
>
> ```typescript theme={null}
> CometChat.getPendingMessages().then(
>   (messages: CometChat.BaseMessage[]) => {
>     messages.forEach((m) => console.log(m.getLocalMessageId(), "->", m.getMessageStatus()));
>   },
>   (error: CometChat.CometChatException) => {
>     console.log("Could not read the outbox:", error.code);
>   }
> );
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> CometChat.getPendingMessages().then(
>   (messages) => {
>     messages.forEach((m) => console.log(m.getLocalMessageId(), "->", m.getMessageStatus()));
>   },
>   (error) => {
>     console.log("Could not read the outbox:", error.code);
>   }
> );
> ```

If the local store is briefly unavailable, the call rejects with `ERROR_OFFLINE_MESSAGE_PERSIST_FAILED` rather than returning an empty list — try again.

### Observe the queue app-wide

The per-call `MessageSentListener` lives only as long as the call. To watch every queue transition — a live send, a reconnect auto-flush, a manual retry, or a resumed send after an app restart — register a global `OfflineMessageListener`. Its failed callback carries the `MessageFailureReason`.

After an app relaunch with a saved login, add this listener straight after `await CometChat.init(...)` resolves, before you await anything else. Messages that fail during startup, for example when the SDK has to rebuild the cache, are delivered just after `init` resolves. A listener added later does not receive them.

> [tab] TypeScript
>
> ```typescript theme={null}
> CometChat.addOfflineMessageListener("LISTENER_ID", {
>   onOutboxMessagePending: (message: CometChat.BaseMessage) => {},
>   onOutboxMessageSent: (message: CometChat.BaseMessage) => {},
>   onOutboxMessageFailed: (message: CometChat.BaseMessage, reason: CometChat.MessageFailureReason) => {
>     console.log("Failed:", message.getLocalMessageId(), reason);
>   },
> });
>
> // Remove it when no longer needed
> CometChat.removeOfflineMessageListener("LISTENER_ID");
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> CometChat.addOfflineMessageListener("LISTENER_ID", {
>   onOutboxMessagePending: (message) => {},
>   onOutboxMessageSent: (message) => {},
>   onOutboxMessageFailed: (message, reason) => {
>     console.log("Failed:", message.getLocalMessageId(), reason);
>   },
> });
>
> // Remove it when no longer needed
> CometChat.removeOfflineMessageListener("LISTENER_ID");
> ```

## Retry and Discard

There is no automatic retry of a `FAILED` message — recovery is explicit and user-invoked. A `FAILED` message stays in the outbox until you retry or discard it. Use the message's local id. Each method takes an optional listener as a second argument; omit it and you get a Promise instead (never both).

* **`retryMessage(localMessageId)`** re-attempts one message. A `FAILED` message is reset to `PENDING` and sent; a `PENDING` message is attempted now rather than waiting for a reconnect. The result is that attempt's outcome.
* **`retryAllFailedMessages()`** retries **transient** failures only; permanent ones are skipped. The Promise form resolves once the retries are issued; pass a `MessageSentListener` to see each outcome.
* **`discardLocalMessage(localMessageId)`** deletes an unsent (`PENDING` or `FAILED`) message from the device, including its copied media. The discard's own listener (`DiscardMessageListener`) gets `onSuccess(localMessageId)`; the original send's listener is released with no final outcome.

> [tab] TypeScript
>
> ```typescript theme={null}
> await CometChat.retryMessage(localMessageId);
>
> await CometChat.retryAllFailedMessages();
>
> CometChat.discardLocalMessage(localMessageId).then(
>   () => console.log("Discarded"),
>   (error: CometChat.CometChatException) => console.log("Discard rejected:", error.code)
> );
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> await CometChat.retryMessage(localMessageId);
>
> await CometChat.retryAllFailedMessages();
>
> CometChat.discardLocalMessage(localMessageId).then(
>   () => console.log("Discarded"),
>   (error) => console.log("Discard rejected:", error.code)
> );
> ```

`retryMessage` and `discardLocalMessage` reject, changing nothing, when:

| Error code | Reason |
| - | - |
| `ERROR_OFFLINE_MESSAGE_IN_FLIGHT` | The message is already sending (`SENDING`). |
| `ERROR_OFFLINE_MESSAGE_NOT_FOUND` | No unsent message has that id (unknown, or already sent). |
| `ERROR_OFFLINE_DISABLED` | Offline support is not running. |
| `ERROR_OFFLINE_MESSAGE_PERSIST_FAILED` | The local cache was briefly unavailable. Try again. |

These codes are available as `CometChat.OfflineErrorCode.MESSAGE_IN_FLIGHT`, `MESSAGE_NOT_FOUND`, `DISABLED` and `MESSAGE_PERSIST_FAILED`.

## Auto-send on Reconnect

By default, queued `PENDING` messages are sent automatically when the realtime connection is re-established. This is controlled by `OfflineConfig.autoSendPendingOnReconnect` (default `true`); see [Enable Offline Support](https://www.cometchat.com/docs/sdk/react-native/offline-enable.md) and the [reference](https://www.cometchat.com/docs/sdk/react-native/offline-reference.md). A queued message that keeps getting no answer is retried on each reconnect, up to 5 times, then becomes `FAILED` (`TRANSIENT`). With it off, nothing retries a queued message on reconnect — a send that gets no answer becomes `FAILED` (`TRANSIENT`), which you recover with `retryMessage`.

Reconnect also runs a catch-up sync for received messages — see [Stay in Sync](https://www.cometchat.com/docs/sdk/react-native/offline-sync.md).

## Queue Limits

The outbox holds at most `maxPendingMessages` messages still trying to send (`PENDING` + `SENDING`); `FAILED` messages do not count. When the queue is full, `OfflineConfig.pendingQueueOverflowPolicy` decides what happens:

| `PendingMessageOverflowPolicy` | Behaviour on overflow |
| - | - |
| `REJECT_NEWEST` (**default**) | The new send is refused and reported with reason `QUEUE_FULL`. Nothing already queued is lost. |
| `DROP_OLDEST` | The oldest `PENDING` message is evicted to admit the new one, and reported with `onError` (code `ERROR_PENDING_QUEUE_LIMIT`, reason `QUEUE_OVERFLOW`). If there is no `PENDING` message to evict, the new send is refused with `QUEUE_FULL`. |

A media send whose bytes do not fit in `maxMediaCacheSize`, even after cached received media is evicted, is refused with reason `BYTE_OVERFLOW`. Queued media is never evicted to make room.

A **refused** send (`QUEUE_FULL` / `BYTE_OVERFLOW`) is never queued, so it fires **no** `onMessagePending`; it reports `onError` with the code `ERROR_PENDING_QUEUE_LIMIT`, and the global `OfflineMessageListener` receives `onOutboxMessageFailed(message, reason)` carrying the exact reason.

> [!WARNING]
>
> A refused or evicted message (`QUEUE_FULL`, `QUEUE_OVERFLOW`, `BYTE_OVERFLOW`) has **no durable row** — it does not appear in `getPendingMessages`, and calling `retryMessage` on its id rejects with `ERROR_OFFLINE_MESSAGE_NOT_FOUND`. Capture these from the failure callback and handle them at that moment. Prefer the default `REJECT_NEWEST`, which never silently drops an already-queued message from another conversation.

## Edits, Deletes, and Reactions Need a Connection

Offline queuing applies to **sending new messages** only. Changes to existing messages — edit, delete, add or remove a reaction — and other own actions are **not queued offline**. They are sent to the server whenever you trigger them, and they reject with the not-connected error if the request gets no answer. Their effect reaches the cache only once the server accepts them.

## Durable Media Uploads

A multi-attachment upload survives an app restart, but it never resumes on its own. Fetch the existing request for a conversation with `getExistingUploadFileRequest` (a Promise that resolves `null` when there is none), and read each file's progress through `getItems()`, which returns a list of items, each with a `state` of `QUEUED`, `UPLOADING`, `UPLOADED`, `FAILED`, or `REJECTED` (which cannot be retried). Retry an individual file with `retryAttachment(fileId)`. With offline on, each item also carries `localCopyPath`, the SDK's copy of the bytes, for previews of a restored tray.

> [tab] TypeScript
>
> ```typescript theme={null}
> const request = await CometChat.getExistingUploadFileRequest("UID", CometChat.RECEIVER_TYPE.USER);
>
> request?.getItems().forEach((item) => {
>   console.log(item.fileId, "->", item.state);
>   if (item.state === "FAILED") {
>     request.retryAttachment(item.fileId);
>   }
> });
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> const request = await CometChat.getExistingUploadFileRequest("UID", CometChat.RECEIVER_TYPE.USER);
>
> request?.getItems().forEach((item) => {
>   console.log(item.fileId, "->", item.state);
>   if (item.state === "FAILED") {
>     request.retryAttachment(item.fileId);
>   }
> });
> ```

See [Media Caching](https://www.cometchat.com/docs/sdk/react-native/offline-media-caching.md) for downloading and caching received attachments.

> [!TIP]
>
> For the full list of offline methods, listeners, enums, model fields, and error codes, see the [Offline Configuration & API Reference](https://www.cometchat.com/docs/sdk/react-native/offline-reference.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.