Error Handling
Torrin provides comprehensive error handling to help you build robust upload experiences. This guide covers error types, handling strategies, and recovery patterns.
Error Types
All Torrin errors extend the TorrinError class:
typescript
import { TorrinError } from '@torrin-kit/core';
class TorrinError extends Error {
code: TorrinErrorCode;
statusCode: number;
details?: Record<string, any>;
}Error Codes
| Code | HTTP | Description |
|---|---|---|
UPLOAD_NOT_FOUND | 404 | Upload session doesn't exist |
UPLOAD_ALREADY_COMPLETED | 409 | Upload already finalized |
UPLOAD_CANCELED | 409 | Upload was canceled by user |
CHUNK_OUT_OF_RANGE | 400 | Invalid chunk index |
CHUNK_SIZE_MISMATCH | 400 | Chunk size doesn't match expected |
CHUNK_HASH_MISMATCH | 400 | Chunk hash validation failed |
MISSING_CHUNKS | 400 | Cannot complete, missing chunks |
STORAGE_ERROR | 500 | Storage operation failed |
INVALID_REQUEST | 400 | Malformed request data |
NETWORK_ERROR | 503 | Network connectivity issue |
INTERNAL_ERROR | 500 | Unexpected server error |
Client-Side Handling
Basic Error Handling
typescript
import { TorrinError } from '@torrin-kit/core';
const upload = torrin.createUpload({ file });
// Via event listener
upload.on('error', (error) => {
if (error instanceof TorrinError) {
console.error(`Error [${error.code}]:`, error.message);
console.error('Details:', error.details);
// Handle specific errors
switch (error.code) {
case 'NETWORK_ERROR':
showRetryDialog();
break;
case 'UPLOAD_CANCELED':
showCanceledMessage();
break;
default:
showGenericError(error.message);
}
}
});
// Via try-catch
try {
const result = await upload.start();
} catch (error) {
if (error instanceof TorrinError) {
handleError(error);
}
}Network Errors
Network errors trigger automatic retry with exponential backoff:
typescript
const torrin = createTorrinClient({
endpoint: '/api/uploads',
retryAttempts: 5, // Retry failed chunks 5 times
retryDelay: 1000, // Start with 1s, doubles each attempt
});
const upload = torrin.createUpload({ file });
upload.on('error', (error) => {
if (error.code === 'NETWORK_ERROR') {
// This only fires after all retries exhausted
showMessage('Upload failed due to network issues. Please check your connection.');
}
});Validation Errors
Handle invalid file or configuration errors:
typescript
try {
const upload = torrin.createUpload({
file,
chunkSize: 500 * 1024 * 1024, // 500MB - might exceed server limit
});
await upload.start();
} catch (error) {
if (error.code === 'INVALID_REQUEST') {
showMessage('File configuration invalid:', error.details);
}
}Cancellation
Distinguish between user cancellation and failures:
typescript
const upload = torrin.createUpload({ file });
cancelButton.onclick = async () => {
await upload.cancel();
};
upload.on('error', (error) => {
if (error.code === 'UPLOAD_CANCELED') {
// User initiated cancellation - not really an error
showMessage('Upload canceled');
} else {
// Actual error
showError(error.message);
}
});Server-Side Handling
Hook-Based Validation
Validate and reject uploads early:
typescript
import { TorrinError } from '@torrin-kit/core';
createTorrinExpressRouter({
storage,
store,
onBeforeInit: async (req, res) => {
// Validate authentication
const token = req.headers.authorization;
if (!token) {
throw new TorrinError(
'Unauthorized upload attempt',
'INVALID_REQUEST',
401,
{ reason: 'Missing authentication token' }
);
}
// Validate file type
const { mimeType } = req.body;
if (!['video/mp4', 'image/jpeg', 'image/png'].includes(mimeType)) {
throw new TorrinError(
'Invalid file type',
'INVALID_REQUEST',
400,
{ allowedTypes: ['video/mp4', 'image/jpeg', 'image/png'] }
);
}
// Validate file size
const { fileSize } = req.body;
const maxSize = 5 * 1024 * 1024 * 1024; // 5GB
if (fileSize > maxSize) {
throw new TorrinError(
'File too large',
'INVALID_REQUEST',
400,
{ maxSize, actualSize: fileSize }
);
}
},
onBeforeChunk: async (req, res) => {
// Rate limiting
const userId = req.user?.id;
const uploads = await getRateLimit(userId);
if (uploads > 100) {
throw new TorrinError(
'Rate limit exceeded',
'INVALID_REQUEST',
429,
{ limit: 100, current: uploads }
);
}
},
onBeforeComplete: async (req, res) => {
// Final validation
const { uploadId } = req.params;
const session = await store.getSession(uploadId);
if (session.receivedChunks.length !== session.totalChunks) {
throw new TorrinError(
'Missing chunks',
'MISSING_CHUNKS',
400,
{
expected: session.totalChunks,
received: session.receivedChunks.length,
}
);
}
},
});Storage Errors
Handle storage failures gracefully:
typescript
import { TorrinError } from '@torrin-kit/core';
class CustomStorageDriver {
async writeChunk(session, index, stream, size) {
try {
await this.s3.upload({ ... });
} catch (err) {
throw new TorrinError(
'Failed to write chunk to S3',
'STORAGE_ERROR',
500,
{
uploadId: session.uploadId,
chunkIndex: index,
originalError: err.message
}
);
}
}
}Express Error Middleware
Catch all errors in Express:
typescript
app.use('/api/uploads', torrinRouter);
// Error handler (must be after torrinRouter)
app.use((err, req, res, next) => {
if (err instanceof TorrinError) {
res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
details: err.details,
},
});
} else {
console.error('Unexpected error:', err);
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
},
});
}
});Recovery Patterns
Auto-Retry with Backoff
Chunks automatically retry, but you can implement full upload retry:
typescript
async function uploadWithRetry(file: File, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const upload = torrin.createUpload({ file });
return await upload.start();
} catch (error) {
if (attempt === maxAttempts) throw error;
const delay = Math.pow(2, attempt) * 1000;
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}Graceful Degradation
Fallback to standard upload on repeated failures:
typescript
async function uploadWithFallback(file: File) {
try {
// Try chunked upload first
const upload = torrin.createUpload({ file });
return await upload.start();
} catch (error) {
if (error.code === 'NETWORK_ERROR') {
// Fallback to standard form upload
console.warn('Chunked upload failed, using standard upload');
return await standardUpload(file);
}
throw error;
}
}User Feedback
Provide actionable feedback:
typescript
upload.on('error', (error) => {
const messages = {
NETWORK_ERROR: {
title: 'Connection Lost',
message: 'Please check your internet connection and try again.',
action: 'Retry',
},
STORAGE_ERROR: {
title: 'Server Error',
message: 'Our servers are experiencing issues. Please try again later.',
action: 'Report Issue',
},
INVALID_REQUEST: {
title: 'Invalid File',
message: error.details?.reason || 'This file cannot be uploaded.',
action: 'Choose Another File',
},
};
const feedback = messages[error.code] || {
title: 'Upload Failed',
message: error.message,
action: 'Try Again',
};
showErrorDialog(feedback);
});Monitoring & Logging
Client-Side Logging
typescript
const upload = torrin.createUpload({ file });
upload.on('progress', (p) => {
logger.debug('Upload progress', {
uploadId: p.uploadId,
percentage: p.percentage,
chunksCompleted: p.chunksCompleted,
});
});
upload.on('error', (error) => {
logger.error('Upload failed', {
code: error.code,
message: error.message,
details: error.details,
uploadId: upload.uploadId,
});
});Server-Side Logging
typescript
createTorrinExpressRouter({
storage,
store,
onBeforeInit: async (req, res) => {
logger.info('Upload initialized', {
fileName: req.body.fileName,
fileSize: req.body.fileSize,
userId: req.user?.id,
});
},
onBeforeComplete: async (req, res) => {
logger.info('Upload completed', {
uploadId: req.params.uploadId,
duration: Date.now() - session.createdAt.getTime(),
});
},
});Testing Error Scenarios
Simulate Network Failures
typescript
// In tests
const mockTorrin = createTorrinClient({
endpoint: '/api/uploads',
retryAttempts: 2, // Lower for faster tests
});
// Mock fetch to fail
global.fetch = jest.fn(() =>
Promise.reject(new Error('Network failure'))
);
const upload = mockTorrin.createUpload({ file });
await expect(upload.start()).rejects.toThrow('NETWORK_ERROR');Test Validation
typescript
createTorrinExpressRouter({
storage,
store,
onBeforeInit: async (req) => {
if (process.env.NODE_ENV === 'test') {
// Inject test failures
if (req.body.metadata?.testFailure === 'invalid_file') {
throw new TorrinError('Test failure', 'INVALID_REQUEST', 400);
}
}
},
});Best Practices
- Always handle errors - Never leave try-catch empty
- Provide context - Include uploadId, fileName in error details
- Log errors - Track failures for debugging and monitoring
- Show actionable messages - Tell users what to do next
- Implement retry logic - Network failures are common
- Validate early - Check file type/size before uploading
- Monitor error rates - Alert on unusual error spikes
- Test failure scenarios - Simulate network issues, storage failures
Next Steps
- Configuration - Configure retries and timeouts
- Resume & Persistence - Recover from interruptions
- TTL & Cleanup - Handle abandoned uploads