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 ​