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 messageHuman-readable error description codeMachine-readable error code for programmatic handling typeError 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_codeHTTP status code messageHuman-readable error description requestId / request_idUnique request ID for support inquiries retryAfter / retry_afterSeconds 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
Webhooks Guide Use webhooks instead of polling for reliable async notifications
Audio Transcription Production streaming with reconnection and error recovery
TypeScript SDK Built-in error handling and automatic retries
Python SDK Error classes and retry configuration