Error handling
Written By Stanislas
Last updated 16 days ago
Best practices and patterns for handling errors when integrating the Swiftask Public Bot API. Learn how to identify errors, implement recovery strategies, and provide user-friendly error messages.
Overview
Errors can occur at multiple levels: REST authentication failures, GraphQL validation errors, network issues, WebSocket disconnections, and server errors. This guide covers how to identify each error type, implement appropriate recovery strategies, and maintain a good user experience when things go wrong.
Proper error handling is critical for production applications. It ensures your app doesn't crash, users understand what went wrong, and the application can recover automatically when possible.
Prerequisites
Before implementing error handling, ensure you have:
Read the foundational guides β Understand Authentication, Chat Operations, and Subscriptions
Apollo Client configured β With proper HTTP and WebSocket links
Understanding of async/await β Error handling uses try-catch blocks
Basic logging setup β For debugging errors in production
Getting started
Here's a minimal error handling setup for the most common errors:
Step 1: Wrap operations in try-catch
Always wrap API calls in try-catch blocks.
async function sendMessageSafely(client, sessionId, message) {
try {
const { data } = await client.mutate({
mutation: SEND_MESSAGE,
variables: {
newMessageData: {
message,
sessionId,
isForAiReply: true,
},
},
});
console.log('Message sent:', data.sendNewMessage.id);
return data.sendNewMessage;
} catch (error) {
console.error('Failed to send message:', error);
throw error;
}
}Step 2: Identify error type
Distinguish between network, GraphQL, and other errors.
function getErrorType(error) {
if (error.networkError) {
return 'NETWORK_ERROR';
}
if (error.graphQLErrors?.length > 0) {
return 'GRAPHQL_ERROR';
}
if (error instanceof TypeError) {
return 'TYPE_ERROR';
}
return 'UNKNOWN_ERROR';
}
// Usage
try {
await sendMessage(sessionId, message);
} catch (error) {
const errorType = getErrorType(error);
console.log('Error type:', errorType);
}Step 3: Show user-friendly messages
Map technical errors to user-friendly messages.
const ERROR_MESSAGES = {
NETWORK_ERROR: 'Unable to connect. Please check your internet connection.',
AUTH_ERROR: 'Authentication failed. Please refresh the page.',
SESSION_NOT_FOUND: 'Chat session not found. Please start a new conversation.',
MESSAGE_SEND_FAILED: 'Failed to send message. Please try again.',
STREAM_ERROR: 'Connection interrupted. Reconnecting...',
RATE_LIMIT: 'Too many requests. Please wait a moment.',
SERVER_ERROR: 'Something went wrong on our end. Please try again later.',
};
function getUserFriendlyError(error) {
if (error.networkError) {
return ERROR_MESSAGES.NETWORK_ERROR;
}
if (error.graphQLErrors?.length > 0) {
const code = error.graphQLErrors[0].extensions?.code;
if (code === 'UNAUTHENTICATED') {
return ERROR_MESSAGES.AUTH_ERROR;
}
if (code === 'NOT_FOUND') {
return ERROR_MESSAGES.SESSION_NOT_FOUND;
}
}
return ERROR_MESSAGES.SERVER_ERROR;
}
// Usage
try {
await sendMessage(sessionId, message);
} catch (error) {
const friendlyMessage = getUserFriendlyError(error);
showErrorToUser(friendlyMessage);
}HTTP authentication errorsError codes and causes
Handling authentication errors
async function authenticateWithErrorHandling(clientToken, clientUuid) {
try {
const response = await fetch(
`https://graphql.swiftask.ai/public/widget-bot/${clientToken}`,
{
method: 'GET',
headers: {
'x-client-uuid': clientUuid,
'Content-Type': 'application/json',
},
}
);
if (!response.ok) {
const error = await response.json();
switch (response.status) {
case 400:
throw new Error('Invalid request: Check your parameters');
case 401:
throw new Error('Authentication failed: Invalid client token');
case 403:
throw new Error('Access denied: Agent may not be public');
case 404:
throw new Error('Agent not found');
case 500:
throw new Error('Server error: Try again later');
default:
throw new Error(`HTTP ${response.status}: ${error.message}`);
}
}
return await response.json();
} catch (error) {
if (error instanceof TypeError) {
// Network error (DNS, timeout, etc.)
throw new Error('Network error: Please check your connection');
}
throw error;
}
}
// Usage
try {
const { data } = await authenticateWithErrorHandling(clientToken, clientUuid);
console.log('Authenticated successfully');
} catch (error) {
console.error('Authentication error:', error.message);
showErrorToUser(error.message);
}GraphQL errors
Understanding GraphQL error structure
GraphQL errors contain detailed information:
// GraphQL error structure
const graphQLError = {
message: 'User not found',
locations: [{ line: 1, column: 8 }],
path: ['sendNewMessage'],
extensions: {
code: 'NOT_FOUND',
exception: {
stacktrace: [...],
},
},
};Common GraphQL error codes
Handling GraphQL errors
import { ApolloError } from '@apollo/client';
async function sendMessageWithErrorHandling(client, sessionId, message) {
try {
const { data } = await client.mutate({
mutation: SEND_MESSAGE,
variables: {
newMessageData: {
message,
sessionId,
isForAiReply: true,
},
},
});
return data.sendNewMessage;
} catch (error) {
if (error instanceof ApolloError) {
// GraphQL errors
if (error.graphQLErrors?.length > 0) {
error.graphQLErrors.forEach((err) => {
const code = err.extensions?.code;
const message = err.message;
console.error('GraphQL Error:', {
code,
message,
path: err.path,
});
// Handle specific error codes
switch (code) {
case 'UNAUTHENTICATED':
console.error('Token expired, re-authenticating...');
handleAuthError();
break;
case 'FORBIDDEN':
console.error('User does not have access');
showErrorToUser('You do not have permission to perform this action');
break;
case 'BAD_USER_INPUT':
console.error('Invalid input:', message);
showErrorToUser(`Invalid input: ${message}`);
break;
case 'NOT_FOUND':
console.error('Resource not found');
showErrorToUser('The requested resource was not found');
break;
case 'RATE_LIMITED':
console.error('Rate limited, retrying...');
showErrorToUser('Too many requests. Please wait a moment.');
break;
default:
console.error('GraphQL error:', message);
showErrorToUser('An error occurred. Please try again.');
}
});
}
// Network errors
if (error.networkError) {
console.error('Network Error:', error.networkError);
handleNetworkError();
}
} else {
// Other errors
console.error('Unexpected error:', error);
showErrorToUser('An unexpected error occurred');
}
throw error;
}
}
function handleAuthError() {
// Clear stored tokens
localStorage.removeItem('swiftask_access_token');
// Redirect to login or refresh page
window.location.reload();
}
function handleNetworkError() {
showErrorToUser('Network error: Please check your connection');
}
function showErrorToUser(message) {
// Display error in UI
console.warn('User message:', message);
}
WebSocket and subscription errors
Connection errors
const wsLink = new WebSocketLink({
uri: 'wss://graphql.swiftask.ai/graphql',
options: {
reconnect: true,
reconnectionAttempts: 5,
connectionCallback: (error) => {
if (error) {
console.error('WebSocket connection error:', error);
showConnectionError('Unable to establish real-time connection');
} else {
console.log('WebSocket connected');
hideConnectionError();
}
},
connectionParams: async () => {
// Refresh token if needed
const token = await getValidToken();
return {
authorization: `Bearer ${token}`,
workspaceId: workspaceId,
};
},
},
});
function showConnectionError(message) {
const banner = document.createElement('div');
banner.className = 'connection-error-banner';
banner.textContent = message;
document.body.prepend(banner);
}
function hideConnectionError() {
const banner = document.querySelector('.connection-error-banner');
if (banner) banner.remove();
}
Subscription error handling
const MESSAGE_STREAM = gql`
subscription OnMessageStream($sessionId: Float!) {
onMessageStream(sessionId: $sessionId) {
messageChunk
botResponseMessageId
isStoppable
}
}
`;
const subscribeToStreamWithErrorHandling = (client, sessionId, onChunk, onError) => {
let fullMessage = '';
let retryCount = 0;
const subscribe = () => {
return client
.subscribe({
query: MESSAGE_STREAM,
variables: { sessionId },
})
.subscribe({
next: (data) => {
fullMessage += data.onMessageStream.messageChunk;
onChunk(fullMessage, data.onMessageStream);
retryCount = 0; // Reset on success
},
error: (error) => {
console.error('Subscription error:', error);
// Determine error type
if (error.message?.includes('authorization')) {
// Auth error - re-authenticate
onError('Authentication expired. Please refresh.');
handleAuthError();
} else if (error.message?.includes('network')) {
// Network error - try to reconnect
retryCount++;
if (retryCount < 3) {
console.log(`Reconnecting... (${retryCount}/3)`);
setTimeout(() => {
subscribe();
}, 5000);
} else {
onError('Connection lost. Please refresh the page.');
}
} else {
// Generic error
onError('Connection error. Please try again.');
}
},
complete: () => {
console.log('Subscription completed');
},
});
};
return subscribe();
};
// Usage
subscribeToStreamWithErrorHandling(
client,
sessionId,
(fullMessage) => {
updateUI(fullMessage);
},
(errorMessage) => {
showErrorToUser(errorMessage);
}
);
Retry strategies
Exponential backoff
Implement automatic retries with increasing delays:
class RetryManager {
constructor(maxRetries = 3, baseDelay = 1000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}
async retry(operation, retryCount = 0) {
try {
return await operation();
} catch (error) {
if (retryCount >= this.maxRetries) {
throw new Error(
`Operation failed after ${this.maxRetries} retries: ${error.message}`
);
}
// Calculate delay with exponential backoff
const delay = this.baseDelay * Math.pow(2, retryCount);
const jitter = Math.random() * 1000; // Add randomness to prevent thundering herd
console.log(
`Retry ${retryCount + 1}/${this.maxRetries} after ${delay + jitter}ms`
);
await this.sleep(delay + jitter);
return this.retry(operation, retryCount + 1);
}
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Usage
const retryManager = new RetryManager(3, 1000);
const result = await retryManager.retry(async () => {
return await sendMessage(sessionId, message);
});
Conditional retry
Only retry on specific error types:
class ConditionalRetry {
constructor(maxRetries = 3) {
this.maxRetries = maxRetries;
}
shouldRetry(error) {
// Don't retry client errors (400-499)
if (error.status >= 400 && error.status < 500) {
// Except 429 (rate limit)
if (error.status !== 429) {
return false;
}
}
// Don't retry auth errors
if (error.message?.includes('Unauthorized') ||
error.message?.includes('Authentication failed')) {
return false;
}
// Retry on network errors
if (error instanceof TypeError || error.networkError) {
return true;
}
// Retry on server errors (500+)
if (error.status >= 500) {
return true;
}
// Retry on rate limit
if (error.status === 429) {
return true;
}
return false;
}
async retry(operation, maxRetries = this.maxRetries) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (!this.shouldRetry(error)) {
throw error;
}
if (i < maxRetries - 1) {
const delay = 1000 * Math.pow(2, i);
console.log(`Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
}
// Usage
const retry = new ConditionalRetry(3);
try {
const result = await retry.retry(() => sendMessage(sessionId, message));
} catch (error) {
console.error('Failed after retries:', error);
}
Token refresh
Implementing automatic token refresh
class TokenManager {
constructor(clientToken) {
this.clientToken = clientToken;
this.clientUuid = this.getOrCreateClientUuid();
this.accessToken = null;
this.tokenExpiry = null;
}
getOrCreateClientUuid() {
let uuid = localStorage.getItem('swiftask_client_uuid');
if (!uuid) {
uuid = crypto.randomUUID();
localStorage.setItem('swiftask_client_uuid', uuid);
}
return uuid;
}
async getValidToken() {
// Check if token is still valid
if (this.accessToken && this.tokenExpiry > Date.now()) {
return this.accessToken;
}
// Refresh token
return await this.refreshToken();
}
async refreshToken() {
console.log('Refreshing authentication token...');
try {
const response = await fetch(
`https://graphql.swiftask.ai/public/widget-bot/${this.clientToken}`,
{
method: 'GET',
headers: {
'x-client-uuid': this.clientUuid,
},
}
);
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const { data } = await response.json();
this.accessToken = data.accessToken;
// Assume token expires in 1 hour (adjust based on actual expiry)
this.tokenExpiry = Date.now() + 60 * 60 * 1000;
console.log('Token refreshed successfully');
return this.accessToken;
} catch (error) {
console.error('Failed to refresh token:', error);
throw error;
}
}
}
// Usage with Apollo Client
const tokenManager = new TokenManager('your_client_token');
const authLink = setContext(async (_, { headers }) => {
try {
const token = await tokenManager.getValidToken();
return {
headers: {
...headers,
authorization: `Bearer ${token}`,
},
};
} catch (error) {
console.error('Failed to get valid token:', error);
// Redirect to login or show error
throw error;
}
});
Complete error handler class
Production-ready error handling for all operations:
class ErrorHandler {
constructor(options = {}) {
this.onAuthError = options.onAuthError || (() => {});
this.onNetworkError = options.onNetworkError || (() => {});
this.onDisplayError = options.onDisplayError || console.error;
this.retryManager = new RetryManager(options.maxRetries || 3);
this.shouldRetry = options.shouldRetry || (() => true);
}
async handleOperation(operation, context = {}) {
try {
return await this.retryManager.retry(operation);
} catch (error) {
return this.handleError(error, context);
}
}
handleError(error, context = {}) {
console.error('Error occurred:', error, 'Context:', context);
// Network errors
if (error.networkError || error instanceof TypeError) {
this.onNetworkError();
this.onDisplayError('Network error: Please check your connection');
return null;
}
// GraphQL errors
if (error.graphQLErrors?.length > 0) {
const gqlError = error.graphQLErrors[0];
const code = gqlError.extensions?.code;
if (code === 'UNAUTHENTICATED') {
this.onAuthError();
this.onDisplayError('Authentication failed. Please refresh the page.');
} else if (code === 'FORBIDDEN') {
this.onDisplayError('You do not have permission to perform this action');
} else if (code === 'BAD_USER_INPUT') {
this.onDisplayError(`Invalid input: ${gqlError.message}`);
} else if (code === 'NOT_FOUND') {
this.onDisplayError('The requested resource was not found');
} else if (code === 'RATE_LIMITED') {
this.onDisplayError('Too many requests. Please wait a moment.');
} else {
this.onDisplayError('An error occurred. Please try again.');
}
return null;
}
// Generic error
this.onDisplayError('An unexpected error occurred');
return null;
}
logError(error, context) {
// Send to logging service (e.g., Sentry, LogRocket)
console.error('Logging error:', {
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString(),
});
}
}
// Usage
const errorHandler = new ErrorHandler({
onAuthError: () => {
// Re-authenticate or redirect to login
window.location.reload();
},
onNetworkError: () => {
// Show offline indicator
showOfflineIndicator();
},
onDisplayError: (message) => {
// Show error to user
showToast(message, 'error');
},
maxRetries: 3,
});
// Use in your code
const result = await errorHandler.handleOperation(
() => sendMessage(sessionId, message),
{
operation: 'sendMessage',
sessionId,
message,
}
);
Practical error handling patterns
Input validation
function validateMessage(message) {
if (!message || message.trim().length === 0) {
throw new Error('Message cannot be empty');
}
if (message.length > 5000) {
throw new Error('Message is too long (max 5000 characters)');
}
return message.trim();
}
const handleSend = async (input) => {
try {
const validatedMessage = validateMessage(input);
await sendMessage(sessionId, validatedMessage);
} catch (error) {
showErrorToUser(error.message);
}
};Session validation
async function getSessionSafely(client, sessionId) {
try {
const { data } = await client.query({
query: GET_SESSION,
variables: { sessionId },
});
if (!data.getOneTodoChatSession) {
throw new Error('Session not found');
}
return data.getOneTodoChatSession;
} catch (error) {
if (error.message === 'Session not found') {
showErrorToUser('This chat session no longer exists');
} else {
showErrorToUser('Failed to load session');
}
throw error;
}
}Graceful degradation
async function sendMessageWithFallback(client, sessionId, message) {
try {
// Try streaming first
return await sendMessage(client, sessionId, message);
} catch (error) {
console.warn('Streaming failed, trying synchronous request:', error);
try {
// Fall back to synchronous request
return await sendSyncMessage(client, sessionId, message);
} catch (fallbackError) {
console.error('Both methods failed:', fallbackError);
throw fallbackError;
}
}
}Best practices
Always handle errors gracefully β Never let errors crash your application
Provide user-friendly messages β Technical errors should be logged, not shown to users
Implement retry logic β Network issues are common; retry with exponential backoff
Log errors for debugging β Include context and stack traces for troubleshooting
Monitor error rates β Track errors to identify systemic issues
Handle token expiration β Implement automatic token refresh
Test error scenarios β Simulate network failures, auth errors, etc. in development
Show loading states β Keep users informed during retries
Provide fallback options β Allow users to retry manually
Clean up resources β Unsubscribe from WebSockets on errors
Use error boundaries β In React, wrap components in error boundaries
Implement circuit breakers β Stop retrying if errors persist
Testing error scenarios
// Simulate network error
const mockNetworkError = () => {
throw new TypeError('Failed to fetch');
};
// Simulate GraphQL error
const mockGraphQLError = () => {
const error = new Error('GraphQL Error');
error.graphQLErrors = [
{
message: 'Unauthenticated',
extensions: { code: 'UNAUTHENTICATED' },
},
];
throw error;
};
// Simulate rate limit
const mockRateLimitError = () => {
const error = new Error('Rate Limited');
error.graphQLErrors = [
{
message: 'Too many requests',
extensions: { code: 'RATE_LIMITED' },
},
];
throw error;
};
// Test your error handler
test('handles network errors', async () => {
const handler = new ErrorHandler();
const result = await handler.handleOperation(mockNetworkError);
expect(result).toBeNull();
});
test('handles auth errors', async () => {
const handler = new ErrorHandler({
onAuthError: jest.fn(),
});
await handler.handleOperation(mockGraphQLError);
expect(handler.onAuthError).toHaveBeenCalled();
});
test('retries on transient errors', async () => {
let attempts = 0;
const operation = async () => {
attempts++;
if (attempts < 3) throw new Error('Temporary error');
return 'success';
};
const result = await new RetryManager().retry(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});