Production integrations must handle errors gracefully to provide a reliable experience. Network issues, rate limits, and temporary server problems are inevitable - your integration should recover from them automatically when possible and fail gracefully when not.
Overview
Why Error Handling Matters
Robust error handling ensures your integration:
- Stays reliable - Automatically recovers from transient failures
- Protects data - Prevents data loss during network interruptions
- Provides visibility - Gives clear feedback when issues occur
- Scales gracefully - Handles rate limits without cascading failures
The Sully.ai SDKs include built-in retry logic for transient errors. This guide covers the underlying concepts and how to customize behavior for production use cases.
All Sully.ai API errors follow a consistent structure:
{
"error": {
"message": "The requested resource was not found",
"code": "resource_not_found",
"type": "invalid_request_error"
}
}
| Field | Description |
|---|
message | Human-readable error description |
code | Machine-readable error code for programmatic handling |
type | Error category (e.g., invalid_request_error, authentication_error, rate_limit_error) |
The HTTP response also includes the appropriate status code in the response header.
HTTP Status Codes
| Code | Meaning | Action |
|---|
| 400 | Bad Request - Invalid input parameters | Fix the request, do not retry |
| 401 | Unauthorized - Invalid or missing API key | Check credentials, do not retry |
| 403 | Forbidden - Not allowed to access resource | Check permissions or account status |
| 404 | Not Found - Resource does not exist | Verify the resource ID |
| 429 | Too Many Requests - Rate limited | Retry with exponential backoff |
| 500 | Internal Server Error - Server-side issue | Retry with exponential backoff |
| 502 | Bad Gateway - Upstream service issue | Retry with exponential backoff |
| 503 | Service Unavailable - Temporary overload | Retry with exponential backoff |
| 504 | Gateway Timeout - Request took too long | Retry with exponential backoff |
Transient vs Permanent Errors
Understanding which errors are transient (temporary) versus permanent is critical for implementing proper retry logic.
Transient Errors (Retry)
These errors are temporary and may succeed if retried:
- 429 - Rate limited, will succeed after backoff
- 500 - Server error, often recovers quickly
- 502/503/504 - Gateway or availability issues
- Network errors - Connection timeouts, DNS failures
Permanent Errors (Do Not Retry)
These errors indicate a problem with the request itself:
- 400 - Bad request parameters
- 401 - Invalid credentials
- 403 - Permission denied
- 404 - Resource not found
function isTransientError(statusCode: number): boolean {
const transientCodes = [429, 500, 502, 503, 504];
return transientCodes.includes(statusCode);
}
function isNetworkError(error: unknown): boolean {
if (error instanceof Error) {
const networkErrors = [
'ECONNRESET',
'ECONNREFUSED',
'ETIMEDOUT',
'ENOTFOUND',
'EAI_AGAIN',
];
return networkErrors.some((code) => error.message.includes(code));
}
return false;
}
function shouldRetry(error: unknown): boolean {
// Network errors are always transient
if (isNetworkError(error)) {
return true;
}
// Check HTTP status code
if (error instanceof APIError) {
return isTransientError(error.status);
}
return false;
}
Retry Strategies
Exponential Backoff with Jitter
The recommended retry strategy uses exponential backoff with random jitter to prevent thundering herd problems:
delay = min(maxDelay, baseDelay * 2^attempt) + random(0, jitter)
- Base delay: Starting wait time (e.g., 1000ms)
- Max delay: Maximum wait time cap (e.g., 30000ms)
- Jitter: Random variation to spread out retries (e.g., 0-25% of delay)
Complete Retry Wrapper
interface RetryConfig {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
jitterFraction: number;
}
const defaultRetryConfig: RetryConfig = {
maxAttempts: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
jitterFraction: 0.25,
};
function calculateDelay(attempt: number, config: RetryConfig): number {
const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt);
const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
const jitter = cappedDelay * Math.random() * config.jitterFraction;
return Math.floor(cappedDelay + jitter);
}
async function withRetry<T>(
operation: () => Promise<T>,
config: Partial<RetryConfig> = {}
): Promise<T> {
const finalConfig = { ...defaultRetryConfig, ...config };
let lastError: Error | undefined;
for (let attempt = 0; attempt < finalConfig.maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
// Don't retry permanent errors
if (!shouldRetry(error)) {
throw error;
}
// Don't wait after the last attempt
if (attempt < finalConfig.maxAttempts - 1) {
const delay = calculateDelay(attempt, finalConfig);
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError ?? new Error('All retry attempts failed');
}
// Usage
const note = await withRetry(
() => client.notes.retrieve(noteId),
{ maxAttempts: 5 }
);
When to Give Up
Set reasonable limits to avoid infinite retry loops:
- Max attempts: 3-5 attempts for most operations
- Max total time: Cap total retry time (e.g., 2 minutes)
- Circuit breaker: After repeated failures, stop retrying temporarily
Never retry indefinitely. Set a maximum number of attempts and handle the final failure gracefully - log the error, notify the user, or queue for manual review.
SDK Error Handling
The Sully.ai SDKs provide specific error classes for different failure scenarios, making it easy to handle errors appropriately.
TypeScript SDK
import SullyAI, {
APIError,
AuthenticationError,
RateLimitError,
BadRequestError,
NotFoundError,
} from '@sullyai/sullyai';
const client = new SullyAI();
async function createNoteWithErrorHandling(transcript: string): Promise<void> {
try {
const note = await client.notes.create({
transcript,
noteType: { type: 'soap' },
});
console.log('Note created:', note.noteId);
} catch (error) {
if (error instanceof AuthenticationError) {
// Invalid API key or account ID - check configuration
console.error('Authentication failed. Check your API credentials.');
// Don't retry - fix credentials first
} else if (error instanceof RateLimitError) {
// Too many requests - SDK handles retries automatically
// For manual handling:
console.error(`Rate limited. Retry after: ${error.retryAfter}s`);
} else if (error instanceof BadRequestError) {
// Invalid request - log and fix the request
console.error('Invalid request:', error.message);
// Don't retry - fix the request parameters
} else if (error instanceof NotFoundError) {
// Resource doesn't exist
console.error('Resource not found:', error.message);
} else if (error instanceof APIError) {
// Other API errors - check if transient
console.error(`API error (${error.status}):`, error.message);
console.error('Request ID:', error.requestId); // Useful for support
} else {
// Unknown error
throw error;
}
}
}
Python SDK
from sullyai import SullyAI
from sullyai.errors import (
APIError,
AuthenticationError,
RateLimitError,
BadRequestError,
NotFoundError,
APIConnectionError,
)
client = SullyAI()
def create_note_with_error_handling(transcript: str) -> None:
try:
note = client.notes.create(
transcript=transcript,
note_type={"type": "soap"},
)
print(f"Note created: {note.note_id}")
except AuthenticationError:
# Invalid API key or account ID - check configuration
print("Authentication failed. Check your API credentials.")
# Don't retry - fix credentials first
except RateLimitError as e:
# Too many requests - SDK handles retries automatically
# For manual handling:
print(f"Rate limited. Retry after: {e.retry_after}s")
except BadRequestError as e:
# Invalid request - log and fix the request
print(f"Invalid request: {e.message}")
# Don't retry - fix the request parameters
except NotFoundError as e:
# Resource doesn't exist
print(f"Resource not found: {e.message}")
except APIConnectionError:
# Network issue - may be transient
print("Failed to connect to API. Check network connection.")
except APIError as e:
# Other API errors
print(f"API error ({e.status_code}): {e.message}")
Error Properties
SDK errors include helpful properties for debugging and logging:
| Property | Description |
|---|
status / status_code | HTTP status code |
message | Human-readable error description |
requestId / request_id | Unique request ID for support inquiries |
retryAfter / retry_after | Seconds to wait before retrying (rate limits) |
WebSocket Error Recovery
Real-time streaming connections require special error handling for connection drops, token expiration, and state recovery.
Connection Drops
WebSocket connections can drop unexpectedly due to network issues. Implement automatic reconnection:
class StreamConnection {
private reconnectAttempt = 0;
private readonly maxReconnectAttempts = 5;
private async handleDisconnect(event: CloseEvent): Promise<void> {
// Normal closure - don't reconnect
if (event.code === 1000) {
return;
}
// Unexpected disconnect - attempt reconnection
if (this.reconnectAttempt < this.maxReconnectAttempts) {
const delay = this.calculateBackoff();
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt + 1})`);
await this.sleep(delay);
this.reconnectAttempt++;
await this.connect();
} else {
console.error('Max reconnection attempts reached');
this.emit('error', new Error('Connection failed after max retries'));
}
}
}
Token Expiration
Streaming tokens have limited validity. Handle expiration gracefully:
ws.onclose = async (event) => {
// 401 close code indicates token expiration
if (event.code === 4001 || event.reason.includes('token')) {
console.log('Token expired, fetching new token...');
// Get fresh token before reconnecting
const newToken = await fetchStreamingToken();
await this.connectWithToken(newToken);
} else {
await this.handleDisconnect(event);
}
};
State Recovery After Reconnection
Buffer audio during reconnection to prevent data loss:
private audioBuffer: string[] = [];
sendAudio(audioData: ArrayBuffer): void {
const base64Audio = this.toBase64(audioData);
if (this.isConnected()) {
this.ws.send(JSON.stringify({ audio: base64Audio }));
} else if (this.isReconnecting()) {
// Buffer audio during reconnection
this.audioBuffer.push(base64Audio);
}
}
private async onReconnected(): Promise<void> {
// Flush buffered audio after reconnection
for (const audio of this.audioBuffer) {
this.ws.send(JSON.stringify({ audio }));
}
this.audioBuffer = [];
}
Timeout Handling
Setting Appropriate Timeouts
Configure timeouts based on operation type:
| Operation | Recommended Timeout |
|---|
| Simple API calls | 30 seconds |
| File uploads (small) | 60 seconds |
| File uploads (large) | 120+ seconds |
| WebSocket connection | 10 seconds |
import SullyAI from '@sullyai/sullyai';
import * as fs from 'fs';
// Default client timeout
const client = new SullyAI({
timeout: 60000, // 60 seconds
});
// Per-request timeout for large files
const transcription = await client.audio.transcriptions.create(
{ audio: fs.createReadStream('large-recording.mp3') },
{ timeout: 180000 } // 3 minutes for large files
);
Handling Timeout Errors
Timeout does not mean failure - the operation may still complete on the server:
import SullyAI, { APIError } from '@sullyai/sullyai';
async function uploadWithTimeoutHandling(
filePath: string
): Promise<string> {
const client = new SullyAI({ timeout: 60000 });
try {
const transcription = await client.audio.transcriptions.create({
audio: fs.createReadStream(filePath),
});
return transcription.transcriptionId;
} catch (error) {
if (error instanceof Error && error.message.includes('timeout')) {
// Timeout occurred - the upload may have succeeded
console.warn('Request timed out. The upload may still be processing.');
console.warn('Check your dashboard or implement idempotency keys.');
// Option 1: Return early and handle async
// Option 2: Retry with longer timeout
// Option 3: Use webhooks to get notified when complete
throw new Error('Upload timed out - check status manually');
}
throw error;
}
}
A timeout error does not mean the operation failed. The server may have received and processed the request. Use webhooks or implement idempotency to handle this safely.
Next Steps