tryError Documentation

API Framework Integration

Learn how to integrate tryError with modern API frameworks for type-safe error handling across client and server

tRPC Integration

tRPC provides end-to-end type safety, but tryError can enhance error handling for non-tRPC operations and provide consistent error patterns.

Server-side Integration

tRPC with tryError on the servertypescript
1import { initTRPC, TRPCError } from '@trpc/server';
2import { tryAsync, isTryError, createError } from '@try-error/core';
3import { z } from 'zod';
4
5const t = initTRPC.create();
6
7// Custom error transformer
8function tryErrorToTRPCError(error: TryError): TRPCError {
9  const errorMap: Record<string, any> = {
10    'ValidationError': 'BAD_REQUEST',
11    'AuthenticationError': 'UNAUTHORIZED',
12    'AuthorizationError': 'FORBIDDEN',
13    'NotFoundError': 'NOT_FOUND',
14    'ConflictError': 'CONFLICT',
15    'RateLimitError': 'TOO_MANY_REQUESTS',
16  };
17
18  return new TRPCError({
19    code: errorMap[error.type] || 'INTERNAL_SERVER_ERROR',
20    message: error.message,
21    cause: error,
22  });
23}
24
25// Helper to convert tryError results to tRPC responses
26const handleTryResult = <T>(result: T | TryError): T => {
27  if (isTryError(result)) {
28    throw tryErrorToTRPCError(result);
29  }
30  return result;
31};
32
33// Create base procedure
34const publicProcedure = t.procedure;
35
36// Example router
37export const userRouter = t.router({
38  getUser: publicProcedure
39    .input(z.object({ id: z.string() }))
40    .query(async ({ input }) => {
41      // Use tryAsync for database operations
42      const userResult = await tryAsync(async () => {
43        const user = await db.user.findUnique({
44          where: { id: input.id }
45        });
46        
47        if (!user) {
48          throw createError({
49            type: 'NotFoundError',
50            message: `User ${input.id} not found`
51          });
52        }
53        
54        return user;
55      });
56
57      return handleTryResult(userResult);
58    }),
59
60  createUser: publicProcedure
61    .input(z.object({
62      name: z.string(),
63      email: z.string().email(),
64    }))
65    .mutation(async ({ input }) => {
66      const result = await tryAsync(async () => {
67        // Check for existing user
68        const existing = await db.user.findUnique({
69          where: { email: input.email }
70        });
71        
72        if (existing) {
73          throw createError({
74            type: 'ConflictError',
75            message: 'User with this email already exists'
76          });
77        }
78
79        return await db.user.create({ data: input });
80      });
81
82      return handleTryResult(result);
83    }),
84});

Client-side Integration

tRPC client with tryErrortypescript
1import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
2import { tryAsync, isTryError } from '@try-error/core';
3
4const trpc = createTRPCProxyClient<AppRouter>({
5  links: [
6    httpBatchLink({
7      url: 'http://localhost:3000/trpc',
8    }),
9  ],
10});
11
12// Wrap tRPC calls with tryAsync for consistent error handling
13export async function fetchUser(userId: string) {
14  const result = await tryAsync(async () => {
15    return await trpc.user.getUser.query({ id: userId });
16  });
17
18  if (isTryError(result)) {
19    // Handle both tRPC errors and network errors consistently
20    console.error('Failed to fetch user:', result);
21    return null;
22  }
23
24  return result;
25}
26
27// React component using the wrapped function
28function UserProfile({ userId }: { userId: string }) {
29  const [user, setUser] = useState(null);
30  const [loading, setLoading] = useState(true);
31
32  useEffect(() => {
33    fetchUser(userId).then(user => {
34      setUser(user);
35      setLoading(false);
36    });
37  }, [userId]);
38
39  if (loading) return <div>Loading...</div>;
40  if (!user) return <div>User not found</div>;
41
42  return <div>{user.name}</div>;
43}

GraphQL Integration

Use tryError with GraphQL resolvers for consistent error handling and type-safe error responses.

Apollo Server Integration

GraphQL resolvers with tryErrortypescript
1import { ApolloServer, gql, ApolloError } from 'apollo-server';
2import { tryAsync, isTryError, createError } from '@try-error/core';
3
4// Type definitions
5const typeDefs = gql`
6  type User {
7    id: ID!
8    name: String!
9    email: String!
10  }
11
12  type Query {
13    user(id: ID!): User
14  }
15
16  type Mutation {
17    createUser(input: CreateUserInput!): User!
18  }
19
20  input CreateUserInput {
21    name: String!
22    email: String!
23  }
24`;
25
26// Convert tryError to ApolloError
27function tryErrorToApolloError(error: TryError): ApolloError {
28  const errorMap: Record<string, string> = {
29    'ValidationError': 'BAD_USER_INPUT',
30    'AuthenticationError': 'UNAUTHENTICATED',
31    'AuthorizationError': 'FORBIDDEN',
32    'NotFoundError': 'NOT_FOUND',
33  };
34
35  return new ApolloError(
36    error.message,
37    errorMap[error.type] || 'INTERNAL_SERVER_ERROR',
38    error.context
39  );
40}
41
42// Resolvers with tryError
43const resolvers = {
44  Query: {
45    user: async (_: any, { id }: { id: string }) => {
46      const result = await tryAsync(async () => {
47        const user = await db.user.findUnique({ where: { id } });
48        
49        if (!user) {
50          throw createError({
51            type: 'NotFoundError',
52            message: `User with id ${id} not found`,
53            context: { userId: id }
54          });
55        }
56        
57        return user;
58      });
59
60      if (isTryError(result)) {
61        throw tryErrorToApolloError(result);
62      }
63
64      return result;
65    },
66  },
67
68  Mutation: {
69    createUser: async (_: any, { input }: { input: any }) => {
70      const result = await tryAsync(async () => {
71        // Validate email uniqueness
72        const existing = await db.user.findUnique({
73          where: { email: input.email }
74        });
75        
76        if (existing) {
77          throw createError({
78            type: 'ValidationError',
79            message: 'Email already in use',
80            context: { field: 'email', value: input.email }
81          });
82        }
83
84        return await db.user.create({ data: input });
85      });
86
87      if (isTryError(result)) {
88        throw tryErrorToApolloError(result);
89      }
90
91      return result;
92    },
93  },
94};
95
96// Create Apollo Server
97const server = new ApolloServer({
98  typeDefs,
99  resolvers,
100  formatError: (err) => {
101    // Log errors for monitoring
102    console.error('GraphQL Error:', err);
103    
104    // Return formatted error to client
105    return {
106      message: err.message,
107      code: err.extensions?.code,
108      ...(process.env.NODE_ENV === 'development' && {
109        extensions: err.extensions
110      })
111    };
112  },
113});

GraphQL Client with tryError

Apollo Client with tryErrortypescript
1import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
2import { tryAsync, isTryError, createError } from '@try-error/core';
3
4const client = new ApolloClient({
5  uri: 'http://localhost:4000/graphql',
6  cache: new InMemoryCache(),
7});
8
9// Wrap GraphQL queries with tryError
10export async function getUser(userId: string) {
11  const result = await tryAsync(async () => {
12    const { data } = await client.query({
13      query: gql`
14        query GetUser($id: ID!) {
15          user(id: $id) {
16            id
17            name
18            email
19          }
20        }
21      `,
22      variables: { id: userId },
23    });
24
25    if (!data.user) {
26      throw createError({
27        type: 'NotFoundError',
28        message: 'User not found'
29      });
30    }
31
32    return data.user;
33  });
34
35  return result;
36}
37
38// React hook with tryError
39export function useUser(userId: string) {
40  const [user, setUser] = useState(null);
41  const [error, setError] = useState<TryError | null>(null);
42  const [loading, setLoading] = useState(true);
43
44  useEffect(() => {
45    getUser(userId).then(result => {
46      if (isTryError(result)) {
47        setError(result);
48      } else {
49        setUser(result);
50      }
51      setLoading(false);
52    });
53  }, [userId]);
54
55  return { user, error, loading };
56}

OpenAPI / REST API Integration

Integrate tryError with OpenAPI-generated clients or custom REST API clients for consistent error handling.

OpenAPI Client Wrapper

Wrapping OpenAPI-generated clientstypescript
1import { tryAsync, createError, TryResult } from '@try-error/core';
2import { UserApi, Configuration } from './generated-api-client';
3
4// Create a wrapper for OpenAPI-generated clients
5export class ApiClient {
6  private userApi: UserApi;
7
8  constructor(baseUrl: string, token?: string) {
9    const config = new Configuration({
10      basePath: baseUrl,
11      headers: token ? { Authorization: `Bearer ${token}` } : {},
12    });
13    
14    this.userApi = new UserApi(config);
15  }
16
17  async getUser(userId: string): Promise<TryResult<User, ApiError>> {
18    const response = await tryAsync(() => this.userApi.getUser(userId));
19    
20    if (isTryError(response)) {
21      // The generated client threw an error - map it to our domain errors
22      const error = response.cause as any;
23      
24      if (error?.response) {
25        const status = error.response.status;
26        const data = error.response.data;
27        
28        switch (status) {
29          case 400:
30            return createError({
31              type: 'ValidationError',
32              message: data.message || 'Invalid request',
33              context: { errors: data.errors }
34            });
35          case 401:
36            return createError({
37              type: 'AuthenticationError',
38              message: 'Authentication required'
39            });
40          case 403:
41            return createError({
42              type: 'AuthorizationError',
43              message: 'Insufficient permissions'
44            });
45          case 404:
46            return createError({
47              type: 'NotFoundError',
48              message: `User ${userId} not found`
49            });
50          case 429:
51            return createError({
52              type: 'RateLimitError',
53              message: 'Too many requests',
54              context: { retryAfter: error.response.headers['retry-after'] }
55            });
56          default:
57            return createError({
58              type: 'ServerError',
59              message: data.message || `Server error: ${status}`,
60              context: { status, data }
61            });
62        }
63      }
64      
65      // Network errors
66      if (error?.request) {
67        return createError({
68          type: 'NetworkError',
69          message: 'Network request failed',
70          context: { originalError: response }
71        });
72      }
73      
74      return response; // Return original error if we can't map it
75    }
76    
77    return response.data;
78  }
79
80  async createUser(userData: CreateUserRequest): Promise<TryResult<User, ApiError>> {
81    const response = await tryAsync(() => this.userApi.createUser(userData));
82    
83    if (isTryError(response)) {
84      // Similar error mapping as getUser
85      return this.mapApiError(response);
86    }
87    
88    return response.data;
89  }
90  
91  private mapApiError(error: TryError): TryError {
92    // Reusable error mapping logic
93    const cause = error.cause as any;
94    if (cause?.response?.status === 409) {
95      return createError({
96        type: 'ConflictError',
97        message: 'Resource already exists',
98        cause: error
99      });
100    }
101    return error;
102  }
103}
104
105// Usage
106const api = new ApiClient('https://api.example.com', authToken);
107
108const userResult = await api.getUser('123');
109if (isTryError(userResult)) {
110  switch (userResult.type) {
111    case 'NotFoundError':
112      // Handle 404
113      break;
114    case 'AuthenticationError':
115      // Redirect to login
116      break;
117    default:
118      // Handle other errors
119  }
120}

Custom REST Client with tryError

Building a REST client with tryErrortypescript
1import { tryAsync, createError, TryResult, TryError } from '@try-error/core';
2
3// Define API-specific error types
4type ApiErrorType = 
5  | 'NetworkError'
6  | 'ValidationError'
7  | 'AuthenticationError'
8  | 'AuthorizationError'
9  | 'NotFoundError'
10  | 'RateLimitError'
11  | 'ServerError';
12
13interface ApiError extends TryError<ApiErrorType> {
14  statusCode?: number;
15  endpoint?: string;
16  requestId?: string;
17}
18
19// REST client with built-in tryError support
20export class RestClient {
21  constructor(
22    private baseUrl: string,
23    private defaultHeaders: Record<string, string> = {}
24  ) {}
25
26  private async request<T>(
27    method: string,
28    path: string,
29    options: RequestInit = {}
30  ): Promise<TryResult<T, ApiError>> {
31    return tryAsync(async () => {
32      const url = `${this.baseUrl}${path}`;
33      const response = await fetch(url, {
34        method,
35        headers: {
36          'Content-Type': 'application/json',
37          ...this.defaultHeaders,
38          ...options.headers,
39        },
40        ...options,
41      });
42
43      const requestId = response.headers.get('x-request-id') || undefined;
44
45      if (!response.ok) {
46        const errorData = await response.json().catch(() => ({}));
47        
48        const errorMap: Record<number, ApiErrorType> = {
49          400: 'ValidationError',
50          401: 'AuthenticationError',
51          403: 'AuthorizationError',
52          404: 'NotFoundError',
53          429: 'RateLimitError',
54        };
55
56        throw createError({
57          type: errorMap[response.status] || 'ServerError',
58          message: errorData.message || `HTTP ${response.status}`,
59          context: {
60            statusCode: response.status,
61            endpoint: url,
62            requestId,
63            details: errorData,
64          }
65        }) as ApiError;
66      }
67
68      return response.json();
69    });
70  }
71
72  get<T>(path: string, options?: RequestInit) {
73    return this.request<T>('GET', path, options);
74  }
75
76  post<T>(path: string, data?: any, options?: RequestInit) {
77    return this.request<T>('POST', path, {
78      ...options,
79      body: data ? JSON.stringify(data) : undefined,
80    });
81  }
82
83  put<T>(path: string, data?: any, options?: RequestInit) {
84    return this.request<T>('PUT', path, {
85      ...options,
86      body: data ? JSON.stringify(data) : undefined,
87    });
88  }
89
90  delete<T>(path: string, options?: RequestInit) {
91    return this.request<T>('DELETE', path, options);
92  }
93}
94
95// Create typed API client
96export class UserApiClient extends RestClient {
97  async getUser(userId: string) {
98    return this.get<User>(`/users/${userId}`);
99  }
100
101  async createUser(userData: CreateUserRequest) {
102    return this.post<User>('/users', userData);
103  }
104
105  async updateUser(userId: string, updates: Partial<User>) {
106    return this.put<User>(`/users/${userId}`, updates);
107  }
108
109  async deleteUser(userId: string) {
110    return this.delete<void>(`/users/${userId}`);
111  }
112}
113
114// Usage with React
115function UserProfile({ userId }: { userId: string }) {
116  const [user, setUser] = useState<User | null>(null);
117  const [error, setError] = useState<ApiError | null>(null);
118
119  useEffect(() => {
120    const api = new UserApiClient('https://api.example.com');
121    
122    api.getUser(userId).then(result => {
123      if (isTryError(result)) {
124        setError(result);
125        
126        // Type-safe error handling
127        if (result.type === 'NotFoundError') {
128          console.log('User not found');
129        } else if (result.type === 'AuthenticationError') {
130          // Redirect to login
131        }
132      } else {
133        setUser(result);
134      }
135    });
136  }, [userId]);
137
138  if (error) {
139    return <ErrorDisplay error={error} />;
140  }
141
142  if (!user) {
143    return <div>Loading...</div>;
144  }
145
146  return <div>{user.name}</div>;
147}

Best Practices for API Integration

1. Consistent Error Types

Define a standard set of error types that map to HTTP status codes. This makes error handling predictable across your entire application.

2. Error Context

Include relevant context in errors (request IDs, endpoints, status codes) to aid in debugging and monitoring.

3. Type Safety

Use TypeScript discriminated unions with tryError to get compile-time guarantees about error handling.

4. Retry Logic

Implement retry logic for transient errors (network issues, rate limits) using tryError's middleware system.

5. Error Monitoring

Use tryError's onError hook to send API errors to monitoring services like Sentry or DataDog.

Migrating Existing APIs

Gradual migration strategytypescript
1// Step 1: Wrap existing API calls with tryAsync
2// Before: API client that throws errors
3class LegacyApiClient {
4  async getUser(id: string) {
5    const response = await fetch(`/api/users/${id}`);
6    if (!response.ok) {
7      throw new Error(`HTTP ${response.status}`);
8    }
9    return response.json();
10  }
11}
12
13// Step 2: Create a wrapper that uses tryAsync
14class ApiWrapper {
15  constructor(private client: LegacyApiClient) {}
16  
17  async getUser(id: string): Promise<TryResult<User, TryError>> {
18    const result = await tryAsync(() => this.client.getUser(id));
19    
20    // If the legacy client threw, enhance the error
21    if (isTryError(result)) {
22      const message = result.message;
23      
24      // Map common HTTP errors to domain errors
25      if (message.includes('HTTP 404')) {
26        return createError({
27          type: 'NotFoundError',
28          message: `User ${id} not found`,
29          cause: result
30        });
31      }
32      
33      if (message.includes('HTTP 401')) {
34        return createError({
35          type: 'AuthenticationError',
36          message: 'Authentication required',
37          cause: result
38        });
39      }
40      
41      // Return enhanced error for other cases
42      return createError({
43        type: 'ApiError',
44        message: result.message,
45        cause: result
46      });
47    }
48    
49    return result;
50  }
51}
52
53// Step 3: Use the wrapped API
54const api = new ApiWrapper(new LegacyApiClient());
55
56// Now all consumers get TryResult instead of exceptions
57const userResult = await api.getUser('123');
58if (isTryError(userResult)) {
59  console.error('Failed to get user:', userResult.message);
60  // No try-catch needed!
61} else {
62  console.log('User:', userResult.name);
63}