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