or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

client-management.mderror-handling.mdindex.mdinterceptors.mdmethod-descriptors.mdstreaming.md
tile.json

error-handling.mddocs/

Error Handling and Status Codes

Comprehensive error handling system with gRPC status codes and HTTP status mapping for robust error management in gRPC-Web applications.

Capabilities

RpcError

gRPC-specific error class that extends the standard Error with status codes and metadata.

/**
 * gRPC-Web error object containing status code, message, and metadata
 */
class RpcError extends Error {
  /**
   * Create a new gRPC error
   * @param code - gRPC status code
   * @param message - Error message
   * @param metadata - Optional error metadata
   */
  constructor(code: StatusCode, message: string, metadata?: Metadata);

  /** gRPC status code */
  code: StatusCode;
  
  /** Error metadata from the server */
  metadata: Metadata;

  /**
   * String representation of the error
   * @returns Formatted error string with status code name
   */
  toString(): string;
}

StatusCode

Enumeration of all standard gRPC status codes with utility functions for HTTP status mapping.

/**
 * gRPC Status Codes enumeration
 */
enum StatusCode {
  /** Not an error; returned on success */
  OK = 0,
  /** The operation was cancelled (typically by the caller) */
  CANCELLED = 1,
  /** Unknown error */
  UNKNOWN = 2,
  /** Client specified an invalid argument */
  INVALID_ARGUMENT = 3,
  /** Deadline expired before operation could complete */
  DEADLINE_EXCEEDED = 4,
  /** Some requested entity was not found */
  NOT_FOUND = 5,
  /** Some entity that we attempted to create already exists */
  ALREADY_EXISTS = 6,
  /** The caller does not have permission to execute the specified operation */
  PERMISSION_DENIED = 7,
  /** Some resource has been exhausted */
  RESOURCE_EXHAUSTED = 8,
  /** Operation was rejected because the system is not in a state required for execution */
  FAILED_PRECONDITION = 9,
  /** The operation was aborted, typically due to a concurrency issue */
  ABORTED = 10,
  /** Operation was attempted past the valid range */
  OUT_OF_RANGE = 11,
  /** Operation is not implemented or not supported/enabled */
  UNIMPLEMENTED = 12,
  /** Internal errors - something is very broken */
  INTERNAL = 13,
  /** The service is currently unavailable */
  UNAVAILABLE = 14,
  /** Unrecoverable data loss or corruption */
  DATA_LOSS = 15,
  /** The request does not have valid authentication credentials */
  UNAUTHENTICATED = 16
}

/**
 * StatusCode utility functions
 */
namespace StatusCode {
  /**
   * Convert HTTP Status code to gRPC Status code
   * @param httpStatus - HTTP status code number
   * @returns Corresponding gRPC status code
   */
  function fromHttpStatus(httpStatus: number): StatusCode;

  /**
   * Convert gRPC Status code to HTTP Status code
   * @param statusCode - gRPC status code
   * @returns Corresponding HTTP status code
   */
  function getHttpStatus(statusCode: StatusCode): number;

  /**
   * Get human-readable name for a status code
   * @param statusCode - gRPC status code
   * @returns Human-readable status name
   */
  function statusCodeName(statusCode: StatusCode): string;
}

Status Interface

Interface representing gRPC status information included in responses and errors.

/**
 * gRPC status information
 */
interface Status {
  /** Status code number */
  code: number;
  /** Status details message */
  details: string;
  /** Optional metadata associated with the status */
  metadata?: Metadata;
}

Usage Examples:

import { 
  GrpcWebClientBase, 
  StatusCode, 
  RpcError 
} from "grpc-web";

const client = new GrpcWebClientBase();

// Basic error handling
try {
  const response = await client.unaryCall(url, request, metadata, methodDescriptor);
  console.log('Success:', response);
} catch (error) {
  if (error instanceof RpcError) {
    console.error('gRPC Error:', error.code, error.message);
    console.error('Error metadata:', error.metadata);
    
    // Handle specific error types
    switch (error.code) {
      case StatusCode.NOT_FOUND:
        console.error('Resource not found');
        break;
      case StatusCode.PERMISSION_DENIED:
        console.error('Access denied');
        break;
      case StatusCode.UNAUTHENTICATED:
        console.error('Authentication required');
        break;
      default:
        console.error('Unexpected error:', StatusCode.statusCodeName(error.code));
    }
  } else {
    console.error('Non-gRPC error:', error);
  }
}

Error Handling Patterns

Retry Logic with Exponential Backoff:

async function callWithRetry<T>(
  callFn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  let lastError: RpcError;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await callFn();
    } catch (error) {
      lastError = error as RpcError;
      
      // Don't retry for these error types
      if (error.code === StatusCode.INVALID_ARGUMENT ||
          error.code === StatusCode.NOT_FOUND ||
          error.code === StatusCode.PERMISSION_DENIED ||
          error.code === StatusCode.UNAUTHENTICATED) {
        throw error;
      }
      
      // Retry for transient errors
      if (attempt < maxRetries && (
          error.code === StatusCode.UNAVAILABLE ||
          error.code === StatusCode.DEADLINE_EXCEEDED ||
          error.code === StatusCode.INTERNAL)) {
        
        const delay = baseDelay * Math.pow(2, attempt);
        console.log(`Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      
      throw error;
    }
  }
  
  throw lastError;
}

// Usage
const response = await callWithRetry(() =>
  client.unaryCall(url, request, metadata, methodDescriptor)
);

Error Context and Logging:

class GrpcErrorHandler {
  static logError(error: RpcError, context: string): void {
    const statusName = StatusCode.statusCodeName(error.code);
    
    console.error(`[${context}] gRPC Error: ${statusName} (${error.code})`);
    console.error(`Message: ${error.message}`);
    
    if (Object.keys(error.metadata).length > 0) {
      console.error('Metadata:', error.metadata);
    }
    
    // Add to monitoring/analytics
    this.reportError(error, context);
  }

  static reportError(error: RpcError, context: string): void {
    // Send to monitoring service
    analytics.track('grpc_error', {
      status_code: error.code,
      status_name: StatusCode.statusCodeName(error.code),
      message: error.message,
      context: context
    });
  }

  static isRetryableError(error: RpcError): boolean {
    return error.code === StatusCode.UNAVAILABLE ||
           error.code === StatusCode.DEADLINE_EXCEEDED ||
           error.code === StatusCode.INTERNAL ||
           error.code === StatusCode.RESOURCE_EXHAUSTED;
  }

  static isClientError(error: RpcError): boolean {
    return error.code === StatusCode.INVALID_ARGUMENT ||
           error.code === StatusCode.NOT_FOUND ||
           error.code === StatusCode.ALREADY_EXISTS ||
           error.code === StatusCode.PERMISSION_DENIED ||
           error.code === StatusCode.UNAUTHENTICATED ||
           error.code === StatusCode.FAILED_PRECONDITION ||
           error.code === StatusCode.OUT_OF_RANGE ||
           error.code === StatusCode.UNIMPLEMENTED;
  }
}

Stream Error Handling:

function handleStreamingCall<T>(
  stream: ClientReadableStream<T>
): Promise<T[]> {
  return new Promise((resolve, reject) => {
    const results: T[] = [];
    let hasErrored = false;

    stream.on('data', (data) => {
      if (!hasErrored) {
        results.push(data);
      }
    });

    stream.on('error', (error: RpcError) => {
      hasErrored = true;
      
      // Log error with context
      GrpcErrorHandler.logError(error, 'streaming_call');
      
      // Handle specific streaming errors
      if (error.code === StatusCode.CANCELLED) {
        console.log('Stream was cancelled');
        resolve(results); // Return partial results
      } else if (error.code === StatusCode.DEADLINE_EXCEEDED) {
        console.log('Stream timeout - returning partial results');
        resolve(results);
      } else {
        reject(error);
      }
    });

    stream.on('end', () => {
      if (!hasErrored) {
        resolve(results);
      }
    });

    // Set timeout for the stream
    setTimeout(() => {
      if (!hasErrored) {
        stream.cancel();
        reject(new RpcError(StatusCode.DEADLINE_EXCEEDED, 'Stream timeout'));
      }
    }, 30000);
  });
}

HTTP Status Code Mapping

gRPC-Web provides utilities to convert between gRPC and HTTP status codes:

// Convert HTTP to gRPC status
const grpcStatus = StatusCode.fromHttpStatus(404); // StatusCode.NOT_FOUND
const grpcStatus2 = StatusCode.fromHttpStatus(500); // StatusCode.UNKNOWN

// Convert gRPC to HTTP status
const httpStatus = StatusCode.getHttpStatus(StatusCode.NOT_FOUND); // 404
const httpStatus2 = StatusCode.getHttpStatus(StatusCode.INTERNAL); // 500

// Get human-readable names
const statusName = StatusCode.statusCodeName(StatusCode.PERMISSION_DENIED); // "PERMISSION_DENIED"

Common Error Scenarios

Authentication Errors:

try {
  const response = await client.unaryCall(url, request, {}, methodDescriptor);
} catch (error) {
  if (error.code === StatusCode.UNAUTHENTICATED) {
    // Redirect to login or refresh token
    await refreshAuthToken();
    // Retry the call
    return client.unaryCall(url, request, { 'authorization': getAuthToken() }, methodDescriptor);
  }
}

Rate Limiting:

try {
  const response = await client.unaryCall(url, request, metadata, methodDescriptor);
} catch (error) {
  if (error.code === StatusCode.RESOURCE_EXHAUSTED) {
    // Check for retry-after in metadata
    const retryAfter = error.metadata['retry-after'];
    if (retryAfter) {
      const delay = parseInt(retryAfter) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
      // Retry the call
      return client.unaryCall(url, request, metadata, methodDescriptor);
    }
  }
}

Service Unavailable:

try {
  const response = await client.unaryCall(url, request, metadata, methodDescriptor);
} catch (error) {
  if (error.code === StatusCode.UNAVAILABLE) {
    // Try fallback service or cached data
    return getFallbackData();
  }
}

Error Metadata

Servers can include additional error information in metadata:

try {
  const response = await client.unaryCall(url, request, metadata, methodDescriptor);
} catch (error) {
  const errorId = error.metadata['error-id'];
  const errorDetails = error.metadata['error-details'];
  
  console.error(`Error ${errorId}: ${error.message}`);
  if (errorDetails) {
    console.error('Details:', errorDetails);
  }
  
  // Report to support with error ID
  reportToSupport(errorId, error);
}