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

# Handle errors and retries

> The three shapes a failure arrives in, how to branch on each, and the five things an agent must never do with one.

This guide shows you how to recognise and handle every failure Coffee Chats can return. It covers the three shapes a failure arrives in, how to tell a retry hint from a real failure, and the mistakes that turn a recoverable failure into lost or duplicated work. The [error codes](/mcp/errors.md) page is the reference table behind it, and [the result object](/mcp/writes.md) defines every field named here.

## Everything below arrives with HTTP status 200

All tool-level failures are returned inside a successful protocol response. Only the transport carries other statuses: 401 for a missing or rejected access credential, 413 for a body over 2 megabytes, and 429 for a rate limit. Branching on HTTP status will not tell you that a write conflicted or that a student declined.

## The three shapes a failure arrives in

**A protocol error**

A read tool's failure carries no code. Almost all of them carry this fixed text:

```json Protocol error envelope theme={"system"}
{
  "isError": true,
  "content": [{ "type": "text", "text": "Tool call failed." }]
}
```

The reason is withheld on purpose: the reason could itself describe data the connection was not allowed to see. A call to a tool whose permission was not granted arrives in exactly this shape, and so does a write that fails before it produces a result of its own.

Two failures say more than that, so read the text before you decide what to do. A lookup of one record by its id, where the account has no such record, names the kind of record it did not find. Eight kinds of record answer that way, and the text tells you to call the matching list tool, which is the action to take. One of the eight is a posting, which no tool lists, so find it with `jobs.search` or ask the student which posting to read. A call to `meetings.transcript.list` for a meeting whose transcript Coffee Chats does not hold says that no transcript is available and why, so the action is to stop asking for that transcript. [Failures that carry no code](/mcp/errors.md) gives all three texts in full.

When the text is the fixed one, there is nothing in the response to branch on. Report the failure to the student and stop.

**A result that carries a failure**

The eighteen writes that take an idempotency key, and `actions.cancel`, answer with the [result object](/mcp/writes.md#the-result-object) instead, and a failing one looks like this:

```json contacts.notes.update result theme={"system"}
{
  "status": "failed",
  "scenario": "conflict",
  "operationId": "0123456789abcdef01234567",
  "error": {
    "code": "VERSION_CONFLICT",
    "retryable": false,
    "correlationId": "0123456789abcdef01234567"
  }
}
```

A failing write result is keyed on `status` and carries no `tag`. The three values of `status` are `failed`, `cancelled` and `uncertain`. A result that has not failed is keyed on `tag` instead, with `requires_confirmation`, `accepted` or `completed`. Read `tag` first, and read `status` when there is no `tag`.

`scenario` is a narrower label on the same outcome and is informational. On a write result it is one of `success`, `loading`, `conflict`, `denied`, `expired`, `unavailable`, `cancelled` or `uncertain`. The first two appear on results that have not failed. The other six appear on failures.

The `code` inside `error` is the thing to branch on, and the [error codes](/mcp/errors.md) page lists every value it can hold. `correlationId` identifies this change in the Coffee Chats logs and carries the same value as `operationId` on every result the connector produces today. Record it and quote it when reporting a problem.

**A retry hint**

A retry hint is not a failure. It never arrives on its own: it rides under `_meta` beside a result whose `tag` is `accepted`, so the whole answer looks like this:

```json Accepted result with a retry hint theme={"system"}
{
  "structuredContent": {
    "tag": "accepted",
    "scenario": "loading",
    "operationId": "0123456789abcdef01234567",
    "operationStatus": "processing"
  },
  "_meta": {
    "coffeechats/retry": {
      "code": "PERMIT_BACKPRESSURE",
      "retryAfterMs": 1000,
      "retryable": true
    }
  }
}
```

The result itself sits under `structuredContent` and the hint under `_meta` of the same answer. The call succeeded in being accepted and nothing has been written yet. Wait for `retryAfterMs` milliseconds and call the tool again with the same idempotency key.

## What `retryable` is worth

Every `error` object carries a `retryable` boolean. It is `false` on every failure the connector maps today, except on `OUTCOME_UNKNOWN`, which copies the flag from the record it is reporting and can therefore arrive as `true`. In a retry hint, `retryable` is fixed at `true` by the contract.

Do not branch on it alone. Use the Action column in the [error codes](/mcp/errors.md) tables, because several codes need a new idempotency key rather than a plain retry, and an uncertain outcome needs a read rather than either.

## How long to keep retrying

The connector sets no retry ceiling and no backoff schedule. The only delay it states is `retryAfterMs` in a retry hint, which the contract bounds to between 1 and 5,000 milliseconds. The following is guidance, not a rule the connector enforces. Honour that delay. Cap the attempts at a small number. Stop once a result is `completed`, `failed`, `cancelled` or `uncertain`, because nothing moves out of those four. One connection may make 120 calls a minute, and a loop spends that budget on one change.

## Three codes that read differently by tool

`VERSION_CONFLICT`, `ACTION_DENIED` and `ACTION_EXPIRED` have a different cause on a safe write, on a proposal and on a withdrawal, because those tools fail for different reasons. Each tool's own page in the [tool reference](/mcp/tools/overview.md) prints the wording that applies to it.

`VERSION_CONFLICT` is the one worth spelling out. On a safe write it means either that the stored value moved under the agent, or that the same idempotency key was sent again with different input. On a proposal it means either that the booking page moved between the proposal and the approval, or, again, that the same key was sent again with different input. On `actions.cancel` it means neither of those: the review request you asked to withdraw was already decided, already expired or already being applied.

## When a cancelled result is visible at all

After the student disconnects, the agent's credential stops working, so most calls are refused before they reach a result. You will therefore rarely see `OPERATION_CANCELLED` from a disconnect. You see it only when the disconnect removed a different connection from the one the agent is using. Treat a refused call as the end of the connection.

A cancelled result from `actions.cancel` is different and always visible, because the agent asked for it on a live connection.

## Five things never to do

> [!WARNING]
>
> Never retry an uncertain outcome under a new key before reading. An uncertain outcome means Coffee Chats cannot tell whether the change landed. A new key is a new change, so retrying under one without reading first can apply the same change twice. [Settle an uncertain result](/mcp/guides/settle-an-uncertain-result.md) gives the read that settles each write.

Never build against a result shape you have not seen documented for a specific tool. The result format allows shapes that no tool returns today, including the `requires_user_action` tag. Use each tool's reference page as the list of shapes it can return, and report anything else as an unrecognised answer instead of acting on it.

Never infer a reason from the fixed read failure text. That text is fixed by design, and an agent that reads a missing permission or a temporary fault into it is guessing. Act only on what a failure says: the text that names a record your account does not have is telling you to call the matching list tool, and `Tool call failed.` is telling you nothing beyond that the call failed. The one exception is a posting, which no tool lists, so there ask the student which posting to read instead: [failures that carry no code](/mcp/errors.md) states that exception in full.

Never keep calling a disconnected connection. Once the student disconnects, the protocol call is refused and renewing is refused too. Stop, and ask the student to authorize again if the work still matters.

Never show a raw code to a student on its own. The codes are for the agent. Say what happened and what is stored now.

## Rate limiting is not an error code

A call refused for rate limiting is answered with HTTP status 429 and these headers: `RateLimit-Policy`, `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and `Retry-After`. None of the published codes appears in that answer. The protocol limiter answers with this body:

```json Rate limit response body theme={"system"}
{ "error": "rate_limited" }
```

Wait the number of seconds in `Retry-After` before calling again. A polling loop that ignores it will spend the whole budget on one waiting change and lock the agent out of its own reads. The [limits](/mcp/limits.md) page states each limit and what it counts.

## Next steps

> **[Error codes](/mcp/errors.md)**
>
> Every code, its cause and the action to take.

> **[Settle an uncertain result](/mcp/guides/settle-an-uncertain-result.md)**
>
> The one failure that needs a read rather than a retry.


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