Skip to content

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

CodeHTTPDescription
UPLOAD_NOT_FOUND404Upload session doesn't exist
UPLOAD_ALREADY_COMPLETED409Upload already finalized
UPLOAD_CANCELED409Upload was canceled by user
CHUNK_OUT_OF_RANGE400Invalid chunk index
CHUNK_SIZE_MISMATCH400Chunk size doesn't match expected
CHUNK_HASH_MISMATCH400Chunk hash validation failed
MISSING_CHUNKS400Cannot complete, missing chunks
STORAGE_ERROR500Storage operation failed
INVALID_REQUEST400Malformed request data
NETWORK_ERROR503Network connectivity issue
INTERNAL_ERROR500Unexpected 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

  1. Always handle errors - Never leave try-catch empty
  2. Provide context - Include uploadId, fileName in error details
  3. Log errors - Track failures for debugging and monitoring
  4. Show actionable messages - Tell users what to do next
  5. Implement retry logic - Network failures are common
  6. Validate early - Check file type/size before uploading
  7. Monitor error rates - Alert on unusual error spikes
  8. Test failure scenarios - Simulate network issues, storage failures

Next Steps