> For the complete documentation index, see [llms.txt](https://titan-exchange.gitbook.io/titan/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://titan-exchange.gitbook.io/titan/developer-doc/swap-api/guides/error-handling.md).

# Error Handling & Reconnect

Handle server errors, stream failures, and connection drops in Titan Direct integrations.

Titan Direct is a persistent WebSocket connection. Connections drop, tokens expire, and streams end unexpectedly. This guide covers every failure mode and how to build a reconnect loop that keeps your integration running.

## Server messages

The server sends one of four message types — `Response`, `Error`, `StreamData`, or `StreamEnd`. See [Types Reference](/titan/developer-doc/swap-api/reference/types.md) for the full type definitions.

## Server errors

When a request fails, the server sends an [`Error`](/titan/developer-doc/swap-api/reference/types.md) instead of a `Response`. Check for the `Error` key in every message handler:

```typescript
ws.on('message', async (raw: Buffer) => {
  const msg = await decodeMessage(raw);

  if ('Error' in msg) {
    const { code, message, requestId } = msg.Error;
    console.error(`Request ${requestId} failed — code ${code}: ${message}`);
    return;
  }
});
```

## Stream errors

A stream can end abnormally. [`StreamEnd`](/titan/developer-doc/swap-api/reference/types.md) carries an optional `errorCode` and `errorMessage` — if present, the stream did not terminate cleanly:

```typescript
ws.on('message', async (raw: Buffer) => {
  const msg = await decodeMessage(raw);

  if ('StreamEnd' in msg) {
    const { id, errorCode, errorMessage } = msg.StreamEnd;

    if (errorCode !== undefined) {
      console.error(`Stream ${id} ended with error ${errorCode}: ${errorMessage}`);
      // Restart the stream or reconnect
    } else {
      console.log(`Stream ${id} ended cleanly`);
    }
  }
});
```

## Connection drops

When the WebSocket closes, all active streams are dead. Listen for the `close` event and trigger your reconnect logic:

```typescript
ws.on('close', (code: number, reason: Buffer) => {
  console.warn(`Connection closed — code: ${code}, reason: ${reason.toString()}`);

  if (code !== 1000) {
    // Abnormal close — reconnect
    scheduleReconnect();
  }
});

ws.on('error', (err: Error) => {
  console.error('WebSocket error:', err.message);
  // 'close' will fire after this
});
```

## Token expiry

The server refuses connections where the JWT `exp` claim is in the past. If your connection is rejected immediately, check that your token is still valid before reconnecting:

```typescript
function isTokenExpired(token: string): boolean {
  const [, payload] = token.split('.');
  const claims = JSON.parse(Buffer.from(payload, 'base64').toString());
  return Date.now() / 1000 > claims.exp;
}
```

Refresh your token before calling `connect()` if it's close to expiry.

## Reconnect with exponential backoff

The SDK has no built-in reconnect — it's your responsibility. Here's a complete reconnect loop:

```typescript
import WebSocket from 'ws';
import { Encoder, Decoder } from '@msgpack/msgpack';
import bs58 from 'bs58';
import { zstdCompress, zstdDecompress } from 'http-encoding';

const encoder = new Encoder({ useBigInt64: true });
const decoder = new Decoder({ useBigInt64: true });

const BASE_DELAY_MS = 1_000;
const MAX_DELAY_MS = 30_000;

let useCompression = false;

async function sendRequest(ws: WebSocket, id: number, data: Record<string, unknown>) {
  const encoded = encoder.encode({ id, data });
  ws.send(useCompression ? await zstdCompress(encoded) : encoded);
}

async function decodeMessage(raw: Buffer): Promise<any> {
  const data = useCompression ? await zstdDecompress(raw) : raw;
  return decoder.decode(data);
}

async function connect(): Promise<WebSocket> {
  const url = `${process.env.TITAN_ENDPOINT}?auth=${process.env.TITAN_API_KEY}`;

  return new Promise((resolve, reject) => {
    const ws = new WebSocket(url, [
      'v1.api.titan.ag+zstd',
      'v1.api.titan.ag',
    ]);

    ws.once('open', () => {
      useCompression = ws.protocol !== 'v1.api.titan.ag';
      resolve(ws);
    });
    ws.once('error', reject);
  });
}

async function runWithReconnect() {
  let attempt = 0;

  while (true) {
    try {
      const ws = await connect();
      console.log('Connected');
      attempt = 0; // reset backoff on success

      // Set up your message handler and streams here
      await setupStreams(ws);

      // Wait for connection to close
      await new Promise<void>((resolve) => ws.once('close', resolve));

    } catch (err) {
      attempt++;
      const delay = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);
      console.warn(`Reconnect attempt ${attempt} in ${delay}ms...`);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

async function setupStreams(ws: WebSocket) {
  let requestId = 0;

  // Call GetInfo first to confirm connection
  sendRequest(ws, requestId++, { GetInfo: {} });

  ws.on('message', async (raw: Buffer) => {
    const msg = await decodeMessage(raw);

    if ('Response' in msg && 'GetInfo' in msg.Response.data) {
      // Connection confirmed — open your streams
      openQuoteStream(ws, requestId++);
    }

    if ('Error' in msg) {
      console.error(`Error ${msg.Error.code}: ${msg.Error.message}`);
    }

    if ('StreamEnd' in msg && msg.StreamEnd.errorCode !== undefined) {
      console.error(`Stream error: ${msg.StreamEnd.errorMessage}`);
      ws.close(); // trigger reconnect
    }
  });
}

async function openQuoteStream(ws: WebSocket, id: number) {
  const SOL  = bs58.decode('So11111111111111111111111111111111111111112');
  const USDC = bs58.decode('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');

  sendRequest(ws, id, {
    NewSwapQuoteStream: {
      swap: {
        inputMint: SOL,
        outputMint: USDC,
        amount: 1_000_000_000n,
        slippageBps: 50,
      },
      transaction: {
        userPublicKey: bs58.decode('YOUR_WALLET_PUBLIC_KEY'),
      },
    },
  });
}

runWithReconnect();
```

{% hint style="warning" %}
Stream IDs do not survive reconnects. After reconnecting, you must open a new stream — the previous stream ID is no longer valid.
{% endhint %}

## Best practices

* Call [`GetInfo`](/titan/developer-doc/swap-api/reference/direct/get-info.md) after every reconnect to confirm the server is reachable before opening streams.
* If a route has `expiresAtMs` or `expiresAfterSlot` set, check these before building a transaction — a quote valid when received can go stale by the time it lands on-chain.
* Keep your reconnect loop separate from your quote processing logic so a stream error doesn't silently kill the reconnect handler.

***

## Related pages

* [Stream & Execute a Swap](/titan/developer-doc/swap-api/guides/stream-and-execute.md) — full guide with transaction building and error handling
* [Error Codes](/titan/developer-doc/swap-api/reference/error-codes.md) — numeric error code reference
* [Connection & Negotiation](/titan/developer-doc/swap-api/reference/direct/connection.md) — protocol negotiation details
* [Types Reference](/titan/developer-doc/swap-api/reference/types.md) — `ServerMessage`, `ResponseError`, `StreamEnd`, and all type definitions
