> ## 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 JavaScript SDK.

> [accordion] AI Integration Quick Reference
>
> ```javascript theme={null}
> // Send with the offline-aware listener (queued when offline, auto-sent on reconnect)
> CometChat.sendMessage(
>   textMessage,
>   new CometChat.MessageSentListener({
>     onMessagePending: (message) => { /* status === PENDING; queued on the device */ },
>     onMessageSent: (message) => { /* server ack: real id + sentAt */ },
>     onError: (error) => { /* FAILED_TO_FETCH = queued offline, not failed */ },
>   })
> ).catch(() => {}); // the returned Promise also rejects while offline
>
> // Read offline state off a message
> message.getLocalMessageId();   // stable client handle while offline is running
> message.getStatus();           // PENDING, SENDING, SENT, DELIVERED, READ, FAILED
> message.getFailureReason();    // MessageFailureReason, set only while FAILED
>
> // Inspect and recover the outbox
> await CometChat.getPendingMessages(conversationId?);          // PENDING + SENDING + FAILED
> await CometChat.retryMessage(localMessageId, sentListener);   // one message
> await CometChat.retryAllFailedMessages(sentListener);         // transient failures only
> await CometChat.discardLocalMessage(localMessageId);          // delete an unsent message
>
> // Observe every outbox transition, app-wide
> CometChat.addOfflineMessageListener("LISTENER_ID", new CometChat.OfflineMessageListener({
>   onOutboxMessagePending: (message) => {},
>   onOutboxMessageSent: (message) => {},
>   onOutboxMessageFailed: (message, reason) => {},
> }));
> ```

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 a page reload or 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.3.0** or later. Queuing only happens once you [enable it](https://www.cometchat.com/docs/sdk/javascript/offline-enable.md) **and** pass a `MessageSentListener`. A bare `await CometChat.sendMessage(message)` with no listener is never queued: offline, it rejects at once, as before.

## Send a Message

Pass a `MessageSentListener` 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 file could not be copied (code `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,
>   new CometChat.MessageSentListener({
>     onMessagePending: (message: CometChat.BaseMessage) => {
>       // Queued and persisted; message.getStatus() === "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) => {
>       // FAILED_TO_FETCH while offline means "queued", not "failed" — see below
>       console.log("Send error:", error.code);
>     },
>   })
> ).catch(() => {});
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> const textMessage = new CometChat.TextMessage(
>   "UID",
>   "Hello!",
>   CometChat.RECEIVER_TYPE.USER
> );
>
> CometChat.sendMessage(
>   textMessage,
>   new CometChat.MessageSentListener({
>     onMessagePending: (message) => {
>       // Queued and persisted; message.getStatus() === "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) => {
>       // FAILED_TO_FETCH while offline means "queued", not "failed" — see below
>       console.log("Send error:", error.code);
>     },
>   })
> ).catch(() => {});
> ```

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

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

> [!NOTE]
>
> The returned Promise is **not** replaced by the listener. It means "was it sent now?": offline it **rejects**, and the eventual send is observed through `onMessageSent` or `getPendingMessages`, never the Promise. Always attach a `.catch()` when you rely on the listener alone.

## Queued, Not Failed

When the device is offline, the send still fires `onMessagePending`, then fires `onError` **once** with the code `FAILED_TO_FETCH`. This is a **notification, not a failure**: the message stays durably `PENDING`, and `onMessageSent` fires **later**, on reconnect, when it actually goes. **Do not re-send on that error.** This is the default behaviour, with `autoSendPendingOnReconnect` on. With it off, an offline send becomes `FAILED` (`TRANSIENT`) right away, and you recover it 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. Read the message's status to tell the two apart — keep the `localMessageId` from `onMessagePending`, then look the message up in the outbox:

| `status` after `onError` | Meaning | What to do |
| - | - | - |
| `PENDING` | Queued; not sent yet. Auto-sends on reconnect (with `autoSendPendingOnReconnect` on, the default). | Show as sending/queued; nothing to do. |
| `FAILED` | A real failure; will not retry on its own. | Offer the user a manual retry (see below). |

> [tab] TypeScript
>
> ```typescript theme={null}
> let localId: string | undefined;
>
> CometChat.sendMessage(
>   textMessage,
>   new CometChat.MessageSentListener({
>     onMessagePending: (message: CometChat.BaseMessage) => {
>       localId = message.getLocalMessageId();
>     },
>     onError: async () => {
>       const outbox = await CometChat.getPendingMessages();
>       const queued = outbox.find((m) => m.getLocalMessageId() === localId);
>       if (queued?.getStatus() === CometChat.MessageStatus.PENDING) {
>         // Queued offline — it will send automatically on reconnect
>       } else if (queued?.getStatus() === CometChat.MessageStatus.FAILED) {
>         // Real failure — surface a retry action
>       }
>     },
>   })
> ).catch(() => {});
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> let localId;
>
> CometChat.sendMessage(
>   textMessage,
>   new CometChat.MessageSentListener({
>     onMessagePending: (message) => {
>       localId = message.getLocalMessageId();
>     },
>     onError: async () => {
>       const outbox = await CometChat.getPendingMessages();
>       const queued = outbox.find((m) => m.getLocalMessageId() === localId);
>       if (queued?.getStatus() === CometChat.MessageStatus.PENDING) {
>         // Queued offline — it will send automatically on reconnect
>       } else if (queued?.getStatus() === CometChat.MessageStatus.FAILED) {
>         // Real failure — surface a retry action
>       }
>     },
>   })
> ).catch(() => {});
> ```

> [!TIP]
>
> For a global view, `onOutboxMessageFailed` on the `OfflineMessageListener` (below) fires only for real failures and carries the reason — a simpler place to drive a "failed, tap to retry" state.

A send made while an earlier message in the **same conversation** is still unsent waits behind it and gets no notice: no `onError`, and its Promise does not reject. It resolves, with `onMessageSent`, when it is sent.

### 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` | Client handle; present while offline is running. |
| `getStatus()` | `MessageStatus` | `PENDING`, `SENDING`, `SENT`, `DELIVERED`, `READ`, `FAILED`. |
| `getFailureReason()` | `MessageFailureReason` | Set only while `FAILED`; otherwise empty. |

`MessageFailureReason` values:

| Reason | When |
| - | - |
| `TRANSIENT` | A temporary server error (5xx, or 429 rate-limited), no answer after five automatic reconnect attempts, or, with `autoSendPendingOnReconnect` off, no answer at all. Retryable. |
| `PERMANENT` | The server rejected the message, the error was not recognized, or copying an attachment into the store timed out. 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 one of its files is larger than `maxMediaCacheSize`, the budget for the media cache. |
| `DISCARDED` | A queued message was removed before it could send: by `clearOfflineCache`, by leaving or deleting its conversation, or because its saved media file is gone. Reported with code `ERROR_OFFLINE_MESSAGE_DISCARDED` on its send listener, and on `onOutboxMessageFailed`. |

## 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. Pass a conversation id to limit it to one conversation. Read each message's `status` to group them.

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

A queued media message sent from local files comes back with its files (`message.files`). With offline off, or nobody signed in, it resolves with an empty list. If the local store is briefly unavailable (busy, locked, full, or still reopening), it **rejects** with `ERROR_OFFLINE_MESSAGE_PERSIST_FAILED` instead of 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 a restart — register a global `OfflineMessageListener`. Its failed callback carries the `MessageFailureReason`.

> [tab] TypeScript
>
> ```typescript theme={null}
> CometChat.addOfflineMessageListener(
>   "LISTENER_ID",
>   new CometChat.OfflineMessageListener({
>     onOutboxMessagePending: (message: CometChat.BaseMessage) => {},
>     onOutboxMessageSent: (message: CometChat.BaseMessage) => {},
>     onOutboxMessageFailed: (message: CometChat.BaseMessage, reason: string) => {
>       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",
>   new CometChat.OfflineMessageListener({
>     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. Each method accepts a message or its `localMessageId`.

* **`retryMessage(message, listener?)`** 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 optional `MessageSentListener` and the returned Promise receive that attempt's outcome. With no answer, it fires the not-connected notice once (`FAILED_TO_FETCH`) and the message stays `PENDING`.
* **`retryAllFailedMessages(listener?)`** retries **transient** failures only; permanent ones are skipped.
* **`discardLocalMessage(message, listener?)`** deletes an unsent (`PENDING` or `FAILED`) message from the device, including its copied media. The optional listener is `{ onSuccess, onError }`; `onSuccess` receives the `localMessageId`.

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

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

A retry is never refused for the queue cap and never evicts other messages, so a retry may take the outbox over `maxPendingMessages`.

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

| Error code | Reason |
| - | - |
| `ERROR_OFFLINE_MESSAGE_IN_FLIGHT` | The message is already sending (`SENDING`, or a timed-out request still awaiting its answer). |
| `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. |

## Auto-send on Reconnect

By default, queued `PENDING` messages are sent automatically when the connection is re-established. This is controlled by `autoSendPendingOnReconnect` (default `true`); see [Enable Offline Support](https://www.cometchat.com/docs/sdk/javascript/offline-enable.md) and the [reference](https://www.cometchat.com/docs/sdk/javascript/offline-reference.md). With it off, a send that gets no answer fails at once with reason `TRANSIENT` (its listener gets one `onError` with `FAILED_TO_FETCH` and the message is `FAILED`), which you recover with `retryMessage`.

A send that gets no answer for 15 seconds (media: 40 seconds plus upload time) counts as unanswered. After five reconnect attempts with no answer it fails with `TRANSIENT`.

Reconnect also runs a catch-up sync for received messages — see [Stay in Sync](https://www.cometchat.com/docs/sdk/javascript/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, `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 (reason `QUEUE_OVERFLOW`) to admit the new one. If there is no `PENDING` message to evict, the new send is refused with `QUEUE_FULL`. |

A media send with a file larger than `maxMediaCacheSize` is refused with reason `BYTE_OVERFLOW`; nothing is evicted to make room.

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

> [!WARNING]
>
> A refused or evicted message (`QUEUE_FULL`, `QUEUE_OVERFLOW`, `BYTE_OVERFLOW`) has **no durable row** — it does not appear in `getPendingMessages`, and `retryMessage` or `discardLocalMessage` 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 fail with the not-connected error if the request gets no answer.

## Durable Media Uploads

A multi-attachment upload survives a restart: the SDK re-attaches to an in-progress batch instead of restarting it. Fetch the existing request for a conversation with `getExistingUploadFileRequest` (it resolves `null` when there is none, or offline is off), read each file's progress with `getItems()`, and retry an individual file with `retryAttachment(fileId)`. Uploads never auto-resume: restored files come back retryable and wait for the user. A `FAILED` file with `localCopyMissing: true` cannot be retried — ask the user to re-add it.

> [tab] TypeScript
>
> ```typescript theme={null}
> const request = await CometChat.getExistingUploadFileRequest("UID", CometChat.RECEIVER_TYPE.USER);
>
> if (request) {
>   for (const item of request.getItems()) {
>     console.log(item.fileId, "->", item.status); // "queued", "uploading", "uploaded", "failed", "rejected", …
>     if (item.status === "failed") {
>       request.retryAttachment(item.fileId);
>     }
>   }
> }
> ```

> [tab] JavaScript
>
> ```javascript theme={null}
> const request = await CometChat.getExistingUploadFileRequest("UID", CometChat.RECEIVER_TYPE.USER);
>
> if (request) {
>   for (const item of request.getItems()) {
>     console.log(item.fileId, "->", item.status); // "queued", "uploading", "uploaded", "failed", "rejected", …
>     if (item.status === "failed") {
>       request.retryAttachment(item.fileId);
>     }
>   }
> }
> ```

See [Media Caching](https://www.cometchat.com/docs/sdk/javascript/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 [Configuration & API Reference](https://www.cometchat.com/docs/sdk/javascript/offline-reference.md).


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