CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-grpc-web

gRPC-Web Client Runtime Library for browser communication with gRPC services

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

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);
}

docs

client-management.md

error-handling.md

index.md

interceptors.md

method-descriptors.md

streaming.md

tile.json