Comprehensive error handling system with gRPC status codes and HTTP status mapping for robust error management in gRPC-Web applications.
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;
}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;
}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);
}
}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);
});
}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"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();
}
}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);
}