---
title: Error Handling
description: Handle rate limits, unsupported features, and other errors from adapters.
type: guide
prerequisites:
  - /docs/usage
---

# Error Handling



Chat SDK throws typed errors for common failures such as rate limits, unsupported features, and lock conflicts. Import the core error classes from the `chat` package.

```typescript
import { ChatError, RateLimitError, NotImplementedError, LockError } from "chat";
```

## Error types

### ChatError

The base class for SDK errors. `RateLimitError`, `NotImplementedError`, and `LockError` all extend `ChatError`. Branch on the machine-readable `code` property:

| Code                     | Thrown by                      | Meaning                                                                                |
| ------------------------ | ------------------------------ | -------------------------------------------------------------------------------------- |
| `NOT_SUPPORTED`          | `bot.openDM`, `bot.getUser`    | The resolved adapter doesn't implement this method                                     |
| `INVALID_THREAD_ID`      | `bot.thread`, internal routing | Thread ID does not match the `adapter:channel:thread` shape                            |
| `INVALID_CHANNEL_ID`     | `bot.channel`                  | Channel ID does not match the `adapter:channel` shape                                  |
| `ADAPTER_NOT_FOUND`      | `bot.thread`, `bot.channel`    | Thread/channel ID references an adapter that wasn't registered on this `Chat` instance |
| `AMBIGUOUS_USER_ID`      | `bot.getUser`, `bot.openDM`    | Numeric user ID could match more than one registered adapter (Discord/Telegram/GitHub) |
| `UNKNOWN_USER_ID_FORMAT` | `bot.getUser`, `bot.openDM`    | The `userId` doesn't match any platform's known ID format                              |
| `RATE_LIMITED`           | Any platform call              | Platform returned 429; see `RateLimitError` below                                      |
| `NOT_IMPLEMENTED`        | Any platform call              | The adapter doesn't implement this feature; see `NotImplementedError` below            |
| `LOCK_FAILED`            | Inbound message routing        | Distributed lock was busy; see `LockError` below                                       |

<TypeTable
  type={{
  message: {
    description: 'Human-readable error message.',
    type: 'string',
  },
  code: {
    description: 'Machine-readable error code (see table above).',
    type: 'string',
  },
  cause: {
    description: 'Original error that caused this one.',
    type: 'unknown | undefined',
  },
}}
/>

### RateLimitError

Thrown when a platform API returns a 429 response. The `retryAfterMs` property tells you how long to wait before retrying.

```typescript title="lib/bot.ts" lineNumbers
import { RateLimitError } from "chat";

try {
  await thread.post("Hello!");
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log(`Rate limited, retry after ${error.retryAfterMs}ms`);
  }
}
```

<TypeTable
  type={{
  code: {
    description: 'Always "RATE_LIMITED".',
    type: 'string',
  },
  retryAfterMs: {
    description: 'Milliseconds to wait before retrying.',
    type: 'number | undefined',
  },
}}
/>

### NotImplementedError

Thrown when you call a feature that a platform doesn't support, such as `addReaction()` on Teams or `schedule()` on an adapter without native scheduling.

```typescript title="lib/bot.ts" lineNumbers
import { NotImplementedError } from "chat";

try {
  await thread.addReaction(emoji.thumbs_up);
} catch (error) {
  if (error instanceof NotImplementedError) {
    console.log(`Feature not supported: ${error.feature}`);
  }
}
```

<TypeTable
  type={{
  code: {
    description: 'Always "NOT_IMPLEMENTED".',
    type: 'string',
  },
  feature: {
    description: 'Name of the unsupported feature.',
    type: 'string | undefined',
  },
}}
/>

See the [feature matrix](/docs/platform-adapters) for which features are supported on each platform.

### LockError

Thrown when the SDK can't acquire the distributed lock it uses to stop two messages in the same thread from being processed at once. This happens under the default `drop` [concurrency strategy](/docs/concurrency). To queue, debounce, or run messages in parallel instead, set the `concurrency` option. To release the held lock instead of throwing, set the deprecated [`onLockConflict`](/docs/usage#configuration-options) option to `'force'`.

<TypeTable
  type={{
  code: {
    description: 'Always "LOCK_FAILED".',
    type: 'string',
  },
}}
/>

## Adapter errors

Adapters also throw specialized errors from the `@chat-adapter/shared` package:

| Error                   | Code                | Description                                               |
| ----------------------- | ------------------- | --------------------------------------------------------- |
| `AdapterRateLimitError` | `RATE_LIMITED`      | Platform rate limit hit, includes `retryAfter` in seconds |
| `AuthenticationError`   | `AUTH_FAILED`       | Invalid or expired credentials                            |
| `ResourceNotFoundError` | `NOT_FOUND`         | Requested resource (channel, message) doesn't exist       |
| `PermissionError`       | `PERMISSION_DENIED` | Bot lacks required permissions/scopes                     |
| `ValidationError`       | `VALIDATION_ERROR`  | Invalid input data (e.g. message too long)                |
| `NetworkError`          | `NETWORK_ERROR`     | Connectivity issue with platform API                      |

## Catching errors

Use `instanceof` to handle specific error types:

```typescript title="lib/bot.ts" lineNumbers
import { RateLimitError, NotImplementedError } from "chat";

bot.onNewMention(async (thread, message) => {
  try {
    await thread.post("Processing...");
    await thread.addReaction(emoji.eyes);
  } catch (error) {
    if (error instanceof RateLimitError) {
      // Wait and retry
      await new Promise((r) => setTimeout(r, error.retryAfterMs ?? 5000));
      await thread.post("Processing...");
    } else if (error instanceof NotImplementedError) {
      // Skip unsupported features gracefully
    } else {
      throw error;
    }
  }
});
```

## Propagating webhook handler errors

Message, action, and slash command handler errors are logged but, by default, tasks passed to `waitUntil` fulfill. Hosts that collect and await those tasks can opt in to observing failures and choose an error response:

```typescript
const tasks: Promise<unknown>[] = [];
const response = await chat.webhooks.slack(request, {
  waitUntil: (task) => tasks.push(task),
  propagateHandlerErrors: true,
});
const results = await Promise.allSettled(tasks);

return results.some((result) => result.status === "rejected")
  ? new Response("Handler failed", { status: 500 })
  : response;
```

This option only exposes handler failures through the supplied promise. It does not automatically change a webhook response, including when using Vercel's native `waitUntil`.

Keep handlers short when awaiting them before responding. [Slack requires acknowledgement within three seconds](https://docs.slack.dev/apis/events-api/#responding-to-events), so move long-running work outside the webhook response path.

Error propagation does not guarantee retries or durable delivery. SDK deduplication can skip redelivered events after a handler fails, so returning `500` alone does not ensure the handler runs again.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)