Async Patterns and Fetch Functionality

Master asynchronous operations and HTTP requests with go-errors

Async Patterns

Promise Timeouts

import { goFetch } from 'go-errors';

// Handle promise timeouts
let [result, err] = await Promise.race([
  goFetch('/api/data'),
  new Promise<[null, Error]>(resolve => 
    setTimeout(() => resolve([null, new Error('timeout')]), 5000)
  )
]);

if (err?.message === 'timeout') {
  console.log('Operation timed out');
}

Multiple Promise Handling

// Handle multiple promises in parallel
let [results, errors] = await Promise.all([
  goFetch('/api/data1'),
  goFetch('/api/data2')
]).then(results => {
  const data = results.map(([d]) => d);
  const errors = results.map(([_, e]) => e).filter(Boolean);
  return [data, errors.length ? errors : null] as const;
});

if (errors) {
  console.error('Some requests failed:', errors);
} else {
  console.log('All data:', results);
}

Response Transformation

interface User {
  id: number;
  firstName: string;
  lastName: string;
}

interface TransformedUser {
  userId: string;
  fullName: string;
}

let [user, err] = await goFetch<TransformedUser>('/api/user', {
  responseTransformer: (data: unknown) => {
    const user = data as User;
    return {
      userId: `user_${user.id}`,
      fullName: `${user.firstName} ${user.lastName}`
    };
  }
});

if (err) {
  console.error('Failed to fetch user:', err);
} else {
  console.log('User:', user);
}

Error Transformation

interface ApiError {
  code: string;
  message: string;
  details?: unknown;
}

let [data, err] = await goFetch<User, ApiError>('/api/user', {
  errorTransformer: (error) => {
    if (error instanceof Response) {
      return {
        code: `HTTP_${error.status}`,
        message: error.statusText,
        details: { status: error.status }
      };
    }
    return {
      code: 'UNKNOWN_ERROR',
      message: error instanceof Error ? error.message : String(error)
    };
  }
});

if (err) {
  console.error(`API Error (${err.code}):`, err.message);
}

Parallel Operations

interface UserProfile {
  user: User;
  posts: Post[];
  settings: UserSettings;
}

async function fetchUserProfile(userId: string): Promise<Result<UserProfile, ApiError>> {
  // Fetch all data in parallel
  const [
    [user, userErr],
    [posts, postsErr],
    [settings, settingsErr]
  ] = await Promise.all([
    goFetch<User>(`/api/users/${userId}`),
    goFetch<Post[]>(`/api/users/${userId}/posts`),
    goFetch<UserSettings>(`/api/users/${userId}/settings`)
  ]);

  // Handle any errors
  if (userErr || postsErr || settingsErr) {
    return [null, {
      code: 'FETCH_ERROR',
      message: 'Failed to fetch user profile',
      details: {
        user: userErr,
        posts: postsErr,
        settings: settingsErr
      }
    }] as const;
  }

  // Return combined data
  return [{
    user,
    posts,
    settings
  }, null] as const;
}

Retry Pattern

interface RetryOptions {
  maxRetries: number;
  initialDelay: number;
  maxDelay: number;
  backoffFactor: number;
  shouldRetry?: (error: unknown) => boolean;
}

async function withRetry<T, E = Error>(
  operation: () => Promise<Result<T, E>>,
  options: RetryOptions
): Promise<Result<T, E>> {
  let delay = options.initialDelay;
  let lastError: E | null = null;

  for (let attempt = 0; attempt < options.maxRetries; attempt++) {
    let [result, err] = await operation();
    if (!err) {
      return [result, null] as const;
    }

    lastError = err;
    if (options.shouldRetry && !options.shouldRetry(err)) {
      break;
    }

    if (attempt < options.maxRetries - 1) {
      await new Promise(resolve => setTimeout(resolve, delay));
      delay = Math.min(delay * options.backoffFactor, options.maxDelay);
    }
  }

  return [null, lastError] as const;
}

// Usage with goFetch
async function fetchWithRetry<T>(url: string) {
  return withRetry<T>(
    () => goFetch<T>(url),
    {
      maxRetries: 3,
      initialDelay: 1000,
      maxDelay: 5000,
      backoffFactor: 2,
      shouldRetry: (error) => {
        if (error instanceof Response) {
          // Retry on network errors or 5xx server errors
          return error.status >= 500;
        }
        return true;
      }
    }
  );
}

let [data, err] = await fetchWithRetry<User>('/api/user/123');

Best Practices

  1. Use Type Parameters

    // ✅ Good: Explicit types
    let [user, err] = await goFetch<User, ApiError>('/api/user');
    
    // ❌ Bad: Implicit any
    let [data, err] = await goFetch('/api/user');
    
  2. Transform Responses

    // ✅ Good: Transform and validate responses
    let [user, err] = await goFetch<User>('/api/user', {
      responseTransformer: (data) => {
        if (!isValidUser(data)) {
          throw new Error('Invalid user data');
        }
        return {
          ...data,
          fullName: `${data.firstName} ${data.lastName}`
        };
      }
    });
    
  3. Handle All Error Cases

    // ✅ Good: Comprehensive error handling
    let [data, err] = await goFetch<User, ApiError>('/api/user', {
      errorTransformer: (error) => ({
        code: error instanceof Response ? `HTTP_${error.status}` : 'UNKNOWN',
        message: error instanceof Error ? error.message : String(error)
      })
    });
    
    if (err) {
      switch (err.code) {
        case 'HTTP_404':
          return handleNotFound();
        case 'HTTP_401':
          return handleUnauthorized();
        default:
          return handleError(err);
      }
    }
    

See Also