Skip to content

Error Handling

Handling errors and exceptions gracefully.

Overview

The library provides structured error handling with custom exceptions and recovery strategies.

MalformedRequestException

The primary exception thrown when API requests fail.

typescript
import { MalformedRequestException } from 'tradingview-screener';

try {
  const results = await screener.get();
} catch (error) {
  if (error instanceof MalformedRequestException) {
    console.error('API Error:', error.responseMsg);
    console.error('Error Code:', error.code);
    console.error('URL:', error.url);
    console.error('Payload:', error.payload);
  }
}

Exception Properties

typescript
class MalformedRequestException extends Error {
  code: number;           // HTTP status code
  responseMsg: string;    // Error message from API
  url: string;           // Request URL
  payload: string;       // Request payload (JSON)
}

Common Error Scenarios

Invalid Field Name

typescript
try {
  screener.select(StockField.PRICE, 'invalid_field');
  const results = await screener.get();
} catch (error) {
  if (error instanceof MalformedRequestException) {
    console.error('Invalid field name');
    // Error code: 400
  }
}

Invalid Filter Values

typescript
try {
  // Negative market cap doesn't make sense
  screener.where(StockField.MARKET_CAPITALIZATION.lt(-1000));
  const results = await screener.get();
} catch (error) {
  if (error instanceof MalformedRequestException) {
    console.error('Invalid filter value');
  }
}

Network Errors

typescript
try {
  const results = await screener.get();
} catch (error) {
  if (error.code === 'ECONNREFUSED') {
    console.error('Cannot connect to TradingView API');
  } else if (error.code === 'ETIMEDOUT') {
    console.error('Request timed out');
  }
}

Rate Limiting

typescript
try {
  const results = await screener.get();
} catch (error) {
  if (error instanceof MalformedRequestException && error.code === 429) {
    console.error('Rate limit exceeded, please wait before retrying');
    // Wait and retry
    await new Promise(resolve => setTimeout(resolve, 5000));
  }
}

Error Recovery Patterns

Retry with Backoff

typescript
async function getWithRetry(
  screener: StockScreener,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<ScreenerResult> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await screener.get();
    } catch (error) {
      lastError = error as Error;

      if (error instanceof MalformedRequestException) {
        // Don't retry client errors (4xx)
        if (error.code >= 400 && error.code < 500) {
          throw error;
        }
      }

      // Exponential backoff
      const delay = baseDelay * Math.pow(2, attempt);
      console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}

// Usage
try {
  const results = await getWithRetry(screener);
  console.table(results.data);
} catch (error) {
  console.error('All retry attempts failed:', error);
}

Fallback Values

typescript
async function getResultsWithFallback(
  screener: StockScreener
): Promise<ScreenerResult> {
  try {
    return await screener.get();
  } catch (error) {
    console.warn('Failed to fetch results, returning empty result set');
    return {
      data: [],
      totalCount: 0,
    };
  }
}

Graceful Degradation

typescript
async function getResultsWithDegradation(
  screener: StockScreener
): Promise<ScreenerResult> {
  try {
    // Try full query
    return await screener.get();
  } catch (error) {
    console.warn('Full query failed, trying simplified query...');

    try {
      // Remove some filters and try again
      const simplified = new StockScreener();
      simplified
        .where(StockField.MARKET_CAPITALIZATION.gt(1e9))
        .setRange(0, 50);

      return await simplified.get();
    } catch (fallbackError) {
      throw error; // Throw original error
    }
  }
}

Validation Before Request

Validate inputs before making requests to catch errors early:

typescript
function validateFilters(screener: StockScreener): boolean {
  // Check if any filters are set
  if (screener['filters'].length === 0) {
    console.warn('No filters set, query may return too many results');
  }

  // Validate range
  const range = screener['range'];
  if (range[1] - range[0] > 1000) {
    console.warn('Requesting more than 1000 results may be slow');
  }

  return true;
}

// Usage
if (validateFilters(screener)) {
  const results = await screener.get();
}

Error Logging

Structured Error Logging

typescript
function logError(error: Error, context: Record<string, any>): void {
  const errorLog = {
    timestamp: new Date().toISOString(),
    error: error.message,
    stack: error.stack,
    ...context,
  };

  if (error instanceof MalformedRequestException) {
    errorLog['apiCode'] = error.code;
    errorLog['apiMessage'] = error.responseMsg;
    errorLog['requestUrl'] = error.url;
  }

  console.error(JSON.stringify(errorLog, null, 2));
}

// Usage
try {
  const results = await screener.get();
} catch (error) {
  logError(error as Error, {
    screenerType: 'stock',
    filterCount: screener['filters'].length,
  });
}

Streaming Error Handling

Automatic Recovery

typescript
for await (const data of screener.stream({ interval: 5000 })) {
  if (data === null) {
    // Error occurred but stream continues
    console.log('Temporary error, waiting for next update...');
    continue;
  }

  console.table(data.data);
}

Manual Error Control

typescript
let consecutiveErrors = 0;
const maxErrors = 3;

for await (const data of screener.stream({ interval: 5000 })) {
  if (data === null) {
    consecutiveErrors++;
    console.error(`Error ${consecutiveErrors}/${maxErrors}`);

    if (consecutiveErrors >= maxErrors) {
      console.error('Too many consecutive errors, stopping stream');
      break;
    }
    continue;
  }

  // Reset on success
  consecutiveErrors = 0;
  console.table(data.data);
}

Stream Timeout

typescript
async function streamWithTimeout(
  screener: StockScreener,
  timeout: number = 60000
): Promise<void> {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Stream timeout')), timeout);
  });

  const streamPromise = (async () => {
    for await (const data of screener.stream({ maxIterations: 10 })) {
      if (data) {
        console.table(data.data);
      }
    }
  })();

  try {
    await Promise.race([streamPromise, timeoutPromise]);
  } catch (error) {
    console.error('Stream error:', error);
  }
}

Partial Results on Error

typescript
async function getAllResultsPartial(
  screener: StockScreener
): Promise<{ data: any[], errors: Error[] }> {
  const pageSize = 150;
  const allResults: any[] = [];
  const errors: Error[] = [];
  let page = 0;

  while (true) {
    try {
      screener.setRange(page * pageSize, (page + 1) * pageSize);
      const results = await screener.get();

      allResults.push(...results.data);

      if (results.data.length < pageSize) {
        break;
      }
    } catch (error) {
      console.error(`Failed to fetch page ${page}:`, error);
      errors.push(error as Error);

      // Continue to next page despite error
      if (errors.length >= 5) {
        console.error('Too many errors, stopping pagination');
        break;
      }
    }

    page++;
  }

  return { data: allResults, errors };
}

// Usage
const { data, errors } = await getAllResultsPartial(screener);
console.log(`Retrieved ${data.length} results with ${errors.length} errors`);

Error Boundary Pattern

typescript
class ScreenerRunner {
  private screener: StockScreener;

  constructor(screener: StockScreener) {
    this.screener = screener;
  }

  async runSafely<T>(
    operation: (screener: StockScreener) => Promise<T>,
    onError?: (error: Error) => T | null
  ): Promise<T | null> {
    try {
      return await operation(this.screener);
    } catch (error) {
      console.error('Operation failed:', error);

      if (onError) {
        return onError(error as Error);
      }

      return null;
    }
  }
}

// Usage
const runner = new ScreenerRunner(screener);

const results = await runner.runSafely(
  async (s) => await s.get(),
  (error) => {
    console.log('Using cached results...');
    return { data: cachedData, totalCount: cachedData.length };
  }
);

Type Guards

typescript
function isMalformedRequestException(
  error: unknown
): error is MalformedRequestException {
  return error instanceof MalformedRequestException;
}

function isNetworkError(error: unknown): boolean {
  return (
    error instanceof Error &&
    ('code' in error) &&
    (error as any).code === 'ECONNREFUSED'
  );
}

// Usage
try {
  const results = await screener.get();
} catch (error) {
  if (isMalformedRequestException(error)) {
    console.error('API error:', error.responseMsg);
  } else if (isNetworkError(error)) {
    console.error('Network error');
  } else {
    console.error('Unknown error:', error);
  }
}

Error Recovery Strategies

Circuit Breaker Pattern

typescript
class CircuitBreaker {
  private failures = 0;
  private lastFailTime = 0;
  private readonly threshold = 3;
  private readonly timeout = 30000; // 30 seconds

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    // Check if circuit is open
    if (this.failures >= this.threshold) {
      const timeSinceLastFail = Date.now() - this.lastFailTime;

      if (timeSinceLastFail < this.timeout) {
        throw new Error('Circuit breaker is OPEN');
      }

      // Try to close circuit
      this.failures = 0;
    }

    try {
      const result = await fn();
      this.failures = 0; // Reset on success
      return result;
    } catch (error) {
      this.failures++;
      this.lastFailTime = Date.now();
      throw error;
    }
  }
}

// Usage
const breaker = new CircuitBreaker();

async function getResultsSafely() {
  try {
    return await breaker.execute(() => screener.get());
  } catch (error) {
    if (error.message === 'Circuit breaker is OPEN') {
      console.log('Service temporarily unavailable');
      return null;
    }
    throw error;
  }
}

Best Practices

1. Always Handle Errors

typescript
// Good: Explicit error handling
try {
  const results = await screener.get();
  processResults(results);
} catch (error) {
  handleError(error);
}

// Bad: No error handling
const results = await screener.get(); // May crash

2. Use Specific Error Types

typescript
// Good: Check error type
if (error instanceof MalformedRequestException) {
  handleApiError(error);
}

// Bad: Generic error handling
console.error('Error:', error);

3. Log Context

typescript
// Good: Log context
console.error('Failed to fetch stocks', {
  filters: screener['filters'].length,
  range: screener['range'],
  error: error.message,
});

// Bad: No context
console.error(error);

4. Don't Swallow Errors

typescript
// Good: Re-throw or handle
try {
  await screener.get();
} catch (error) {
  console.error('Query failed:', error);
  throw error; // Let caller handle
}

// Bad: Silent failure
try {
  await screener.get();
} catch (error) {
  // Nothing - error is lost
}

Testing Error Handling

typescript
import { describe, it, expect, vi } from 'vitest';

describe('Error Handling', () => {
  it('should handle API errors', async () => {
    const screener = new StockScreener();

    // Mock HTTP to throw error
    vi.spyOn(screener as any, '_makeRequest').mockRejectedValue(
      new MalformedRequestException(400, 'Invalid request', 'url', '{}')
    );

    await expect(screener.get()).rejects.toThrow(MalformedRequestException);
  });

  it('should retry on network error', async () => {
    const screener = new StockScreener();
    let attempts = 0;

    vi.spyOn(screener as any, '_makeRequest').mockImplementation(() => {
      attempts++;
      if (attempts < 3) {
        throw new Error('Network error');
      }
      return Promise.resolve({ data: [], totalCount: 0 });
    });

    const results = await getWithRetry(screener);
    expect(attempts).toBe(3);
  });
});

Next Steps

Released under the MIT License.