class HttpError extends Error { constructor(message: string, public status: number, public statusText: string, public data?: any) { super(message); this.name = 'HttpError'; } } async function safeFetch(url: string, options?: RequestInit): Promise { try { const response = await fetch(url, options); if (!response.ok) { let errorData: any; try { errorData = await response.json(); // Attempt to parse JSON error details } catch (jsonError) { // If JSON parsing fails, use raw text or no data errorData = await response.text().catch(() => null); } throw new HttpError( `HTTP error! Status: ${response.status}`, response.status, response.statusText, errorData ); } // Handle cases where response might be empty (e.g., 204 No Content) const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); } else { // If not JSON, return as text or null return await response.text() as T; // Or handle other content types specifically } } catch (error) { if (error instanceof TypeError && error.message === 'Failed to fetch') { // Network error (e.g., no internet, CORS issues, server down) throw new Error('Network error or server unreachable.'); } // Re-throw known HttpError or other unexpected errors throw error; } } // Usage example: async function fetchData() { try { const data = await safeFetch<{ message: string }>('https://api.example.com/data'); console.log('Success:', data.message); } catch (error) { if (error instanceof HttpError) { console.error(`API Error: ${error.message} (Status: ${error.status})`); console.error('Error details:', error.data); } else if (error instanceof Error) { console.error('General Error:', error.message); } else { console.error('An unknown error occurred.'); } } } fetchData();