tryError Documentation

Common Pitfalls

Learn from common mistakes and avoid gotchas when using tryError.

Critical
Accessing Error Properties Directly
Most common TypeScript error when working with tryError

❌ Wrong Approach

Direct property accesstypescript
const result = await tryAsync(() => fetchUserData());

if (!isOk(result)) {
  // ❌ TypeScript Error: Property 'message' does not exist on type 'never'
  console.log(result.message);
  console.log(result.type);
}

✅ Correct Approach

Use proper type guardstypescript
const result = await tryAsync(() => fetchUserData());

if (isTryError(result)) {
  // ✅ Type-safe access to error properties
  console.log(result.message);
  console.log(result.type);
  console.log(result.source);
}

// Alternative pattern
if (isErr(result)) {
  // result is guaranteed to be TryError
  handleError(result);
}

Why this happens: TypeScript sees TryResult<T, E> as a union type. When you check !isOk(result), TypeScript can't narrow the type to just the error case. Use isTryError(result) or isErr(result) for proper type narrowing.

Critical
Wrapping Every Function Call
Over-engineering with tryError leads to performance issues and code bloat

❌ Over-wrapping Functions

Unnecessary wrappingtypescript
// ❌ Don't wrap every operation
function processUser(user: User) {
  const nameResult = trySync(() => user.name.toUpperCase());
  const emailResult = trySync(() => user.email.toLowerCase());
  const ageResult = trySync(() => user.age + 1);
  
  // This adds unnecessary overhead for simple operations
  if (isOk(nameResult) && isOk(emailResult) && isOk(ageResult)) {
    return { name: nameResult, email: emailResult, age: ageResult };
  }
}

✅ Strategic Use of tryError

Wrap risky operations onlytypescript
// ✅ Only wrap operations that can actually fail
function processUser(user: User) {
  // Simple operations that won't throw - no wrapping needed
  const name = user.name.toUpperCase();
  const email = user.email.toLowerCase();
  const age = user.age + 1;
  
  // Wrap the risky validation step
  const validationResult = trySync(() => validateUserData({ name, email, age }));
  
  if (isTryError(validationResult)) {
    return validationResult; // Return error
  }
  
  return { name, email, age }; // Return success
}

// ✅ Or wrap the entire operation
function processUserSafely(user: User) {
  return trySync(() => {
    const validation = validateUserData(user);
    if (!validation.valid) {
      throw new Error(validation.message);
    }
    
    return {
      name: user.name.toUpperCase(),
      email: user.email.toLowerCase(),
      age: user.age + 1
    };
  });
}

Performance tip: tryError has zero overhead for the success path, but each wrapper function call still has JavaScript function call overhead. Use it strategically for operations that can actually fail.

Common
Mixing Error Handling Paradigms
Inconsistent error handling creates confusion and bugs

❌ Inconsistent Error Handling

Mixed paradigmstypescript
// ❌ Mixing try/catch with tryError
async function fetchAndProcess(id: string) {
  try {
    const userData = await tryAsync(() => fetchUser(id));
    
    if (isTryError(userData)) {
      throw new Error(userData.message); // Converting back to exception
    }
    
    // Now mixing with regular try/catch
    const processed = await processUserData(userData);
    return processed;
  } catch (error) {
    // This catches both thrown errors and any unhandled exceptions
    console.error(error);
    return null;
  }
}

✅ Consistent Error-as-Values

Pure tryError approachtypescript
// ✅ Pure tryError approach
async function fetchAndProcess(id: string): Promise<TryResult<ProcessedUser>> {
  const userData = await tryAsync(() => fetchUser(id));
  
  if (isTryError(userData)) {
    return userData; // Propagate error
  }
  
  const processedData = await tryAsync(() => processUserData(userData));
  
  if (isTryError(processedData)) {
    return processedData; // Propagate error
  }
  
  return processedData; // Return success
}

// ✅ Or use chaining for cleaner code
async function fetchAndProcessChained(id: string): Promise<TryResult<ProcessedUser>> {
  return tryChainAsync(
    await tryAsync(() => fetchUser(id)),
    (userData) => tryAsync(() => processUserData(userData))
  );
}
Common
Ignoring Loading States in React
Poor user experience from missing loading indicators

❌ No Loading States

Missing loading indicatorstypescript
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      const result = await tryAsync(() => api.getUser(userId));
      
      if (isTryError(result)) {
        setError(result.message);
      } else {
        setUser(result);
      }
    };
    
    fetchUser();
  }, [userId]);

  // ❌ No loading state - user sees blank screen during fetch
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>; // This shows before loading
  
  return <UserDisplay user={user} />;
}

✅ Proper Loading States

Complete state managementtypescript
function UserProfile({ userId }: { userId: string }) {
  const [state, setState] = useState<{
    user: User | null;
    loading: boolean;
    error: TryError | null;
  }>({
    user: null,
    loading: true,
    error: null
  });

  useEffect(() => {
    const fetchUser = async () => {
      setState(prev => ({ ...prev, loading: true, error: null }));
      
      const result = await tryAsync(() => api.getUser(userId));
      
      if (isTryError(result)) {
        setState({ user: null, loading: false, error: result });
      } else {
        setState({ user: result, loading: false, error: null });
      }
    };
    
    fetchUser();
  }, [userId]);

  // ✅ Clear loading states and error handling
  if (state.loading) return <LoadingSpinner />;
  if (state.error) return <ErrorDisplay error={state.error} />;
  if (!state.user) return <NotFound />;
  
  return <UserDisplay user={state.user} />;
}

// ✅ Even better: use the useTry hook
function UserProfileWithHook({ userId }: { userId: string }) {
  const { data: user, error, loading } = useTry(
    () => api.getUser(userId),
    [userId]
  );

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorDisplay error={error} />;
  if (!user) return <NotFound />;
  
  return <UserDisplay user={user} />;
}
Common
Not Handling Partial Failures
All-or-nothing error handling misses nuanced failure scenarios

❌ All-or-Nothing Approach

Binary failure handlingtypescript
// ❌ Fails completely if any part fails
async function loadDashboardData() {
  const userResult = await tryAsync(() => fetchUser());
  const postsResult = await tryAsync(() => fetchUserPosts());
  const settingsResult = await tryAsync(() => fetchUserSettings());
  
  // If any fails, the whole operation fails
  if (isTryError(userResult) || isTryError(postsResult) || isTryError(settingsResult)) {
    return createError({ type: 'DashboardError', message: 'Failed to load dashboard' });
  }
  
  return { user: userResult, posts: postsResult, settings: settingsResult };
}

✅ Graceful Partial Failures

Handle partial failures gracefullytypescript
// ✅ Handle partial failures gracefully
async function loadDashboardData() {
  const [userResult, postsResult, settingsResult] = await Promise.all([
    tryAsync(() => fetchUser()),
    tryAsync(() => fetchUserPosts()),
    tryAsync(() => fetchUserSettings())
  ]);
  
  // User data is critical - fail if missing
  if (isTryError(userResult)) {
    return userResult;
  }
  
  // Posts and settings are optional - provide defaults
  const posts = isTryError(postsResult) ? [] : postsResult;
  const settings = isTryError(settingsResult) ? getDefaultSettings() : settingsResult;
  
  return {
    user: userResult,
    posts,
    settings,
    errors: {
      posts: isTryError(postsResult) ? postsResult : null,
      settings: isTryError(settingsResult) ? settingsResult : null
    }
  };
}
Common
Forgetting Error Context
Generic error messages make debugging difficult

❌ Generic Error Messages

Unhelpful error contexttypescript
// ❌ Generic, unhelpful errors
async function updateUserProfile(userId: string, data: UserUpdate) {
  const result = await tryAsync(() => 
    fetch(`/api/users/${userId}`, {
      method: 'PUT',
      body: JSON.stringify(data)
    })
  );
  
  if (isTryError(result)) {
    // Generic error - hard to debug
    return createError({ type: 'UpdateError', message: 'Update failed' });
  }
  
  return result.json();
}

✅ Rich Error Context

Detailed error informationtypescript
// ✅ Rich error context for better debugging
async function updateUserProfile(userId: string, data: UserUpdate) {
  const result = await tryAsync(() => 
    fetch(`/api/users/${userId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
  );
  
  if (isTryError(result)) {
    return createError({
      type: 'NetworkError',
      message: `Failed to update user profile: ${result.message}`,
      context: {
        userId,
        operation: 'updateProfile',
        endpoint: `/api/users/${userId}`,
        requestData: data,
        originalError: result
      }
    });
  }
  
  if (!result.ok) {
    const errorData = await result.text();
    return createError({
      type: 'ApiError',
      message: `Server error: ${result.status} ${result.statusText}`,
      context: {
        userId,
        status: result.status,
        statusText: result.statusText,
        responseBody: errorData,
        endpoint: `/api/users/${userId}`
      }
    });
  }
  
  return result.json();
}
Critical
Unsafe Error Context Access
Error context is typed as `unknown` for type safety - here's how to handle it properly

❌ Unsafe Context Access

Direct context property accesstypescript
const result = await tryAsync(() => validateUserInput(data));

if (isTryError(result)) {
  // ❌ TypeScript Error: Property 'field' does not exist on type 'unknown'
  console.log(result.context.field);
  
  // ❌ Runtime Error: Cannot read properties of undefined
  console.log(result.context.validationRules);
}

✅ Type-Safe Context Access

Proper context validationtypescript
// Define your context interface
interface ValidationContext {
  field: string;
  value: unknown;
  rule: string;
}

// Type guard for validation
function isValidationContext(context: unknown): context is ValidationContext {
  return typeof context === 'object' && 
         context !== null && 
         'field' in context &&
         'rule' in context &&
         typeof (context as any).field === 'string';
}

const result = await tryAsync(() => validateUserInput(data));

if (isTryError(result)) {
  // ✅ Safe context access with type checking
  if (isValidationContext(result.context)) {
    console.log(`Validation failed for field: ${result.context.field}`);
    console.log(`Rule: ${result.context.rule}`);
    console.log(`Value: ${result.context.value}`);
  } else {
    console.log('Error occurred but context format is unknown');
  }
}

✅ Simplified Pattern for Known Context

Quick type assertion with validationtypescript
const result = await tryAsync(() => validateUserInput(data));

if (isTryError(result)) {
  // ✅ Quick check with fallback
  const context = result.context as { field?: string; rule?: string } | undefined;
  
  if (context?.field && context?.rule) {
    console.log(`Validation failed: ${context.field} (${context.rule})`);
  } else {
    console.log(`Validation failed: ${result.message}`);
  }
}

✅ Generic Context Helper

Reusable context extractiontypescript
// Utility function for safe context access
function getContextValue<T>(
  error: TryError, 
  key: string, 
  defaultValue: T
): T {
  if (typeof error.context === 'object' && 
      error.context !== null && 
      key in error.context) {
    return (error.context as any)[key] ?? defaultValue;
  }
  return defaultValue;
}

// Usage
const result = await tryAsync(() => validateUserInput(data));

if (isTryError(result)) {
  const field = getContextValue(result, 'field', 'unknown');
  const rule = getContextValue(result, 'rule', 'unknown');
  
  console.log(`Validation failed for ${field} (rule: ${rule})`);
}

Why context is unknown: Error context is intentionally typed as unknown because it can contain any data structure depending on where the error originated. This forces you to validate the shape before accessing properties, preventing runtime errors from unexpected context structures.

Best practices for context:
• Always validate context structure before accessing properties
• Create type guards for your common context patterns
• Provide fallback values when context is missing or malformed
• Document your error context interfaces for team consistency

Quick Checklist
Avoid these common mistakes for better tryError usage

✅ Best Practices

  • • Use isTryError() or isErr() for type narrowing
  • • Only wrap operations that can actually fail
  • • Maintain consistent error handling throughout your app
  • • Always show loading states in UI components
  • • Handle partial failures gracefully when possible
  • • Include rich context in error objects
  • • Validate error context before accessing properties
  • • Use error boundaries for unexpected errors
  • • Provide retry mechanisms for transient failures

❌ Common Mistakes

  • • Accessing error properties without type guards
  • • Wrapping every single function call
  • • Mixing try/catch with tryError paradigms
  • • Forgetting to handle loading states
  • • Using all-or-nothing error handling
  • • Creating generic, unhelpful error messages
  • • Accessing error context without type validation
  • • Not testing error paths in your code
  • • Ignoring TypeScript strict mode warnings