npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@pbpeterson/typed-fetch

v0.6.4

Published

A type-safe HTTP client that never throws. Inspired by Go's error handling pattern.

Downloads

42

Readme

@pbpeterson/typed-fetch

A type-safe HTTP client for TypeScript that never throws. Inspired by Go's error handling pattern, built on top of the native Fetch API.

Why typed-fetch?

Traditional fetch libraries throw exceptions on HTTP errors, making error handling cumbersome and error-prone. typed-fetch follows Go's philosophy of explicit error handling - errors are values, not exceptions.

// ❌ Traditional approach - can throw unexpectedly
try {
  const response = await fetch('/api/users');
  const users = await response.json(); // What if response is 404?
} catch (error) {
  // Handle network errors, parsing errors, HTTP errors... all mixed together
}

// ✅ typed-fetch approach - explicit and type-safe
const { response, error } = await typedFetch<User[]>('/api/users');
if (error) {
  // Handle error with full type information
  console.log(`HTTP ${error.status}: ${error.statusText}`);
  const errorDetails = await error.json(); // Access error response body
} else {
  // TypeScript knows response is not null
  const users = await response.json(); // Type: User[]
}

Features

  • 🚫 Never throws - All errors are returned as values
  • 🎯 Fully typed - Complete TypeScript support for responses and errors
  • 🔧 Built on Fetch - Thin wrapper around the native Fetch API
  • 📝 Comprehensive HTTP Error Classes - 40+ specific error types covering all standard HTTP status codes
  • 🌐 Network Error Handling - Separate handling for network vs HTTP errors
  • 🎨 Customizable Error Types - Bring your own error interfaces
  • 📦 Minimal Dependencies - Only uses is-network-error for reliable network error detection
  • 🔍 Static Properties - Access status codes without instantiation via TypedFetchErrors.NotFound.status

Installation

npm install @pbpeterson/typed-fetch
# or
pnpm add @pbpeterson/typed-fetch
# or
yarn add @pbpeterson/typed-fetch

Basic Usage

Simple GET Request

import { typedFetch } from '@pbpeterson/typed-fetch';

interface User {
  id: number;
  name: string;
  email: string;
}

const { response, error } = await typedFetch<User[]>('/api/users');

if (error) {
  console.error('Failed to fetch users:', error.statusText);
} else {
  const users = await response.json(); // Type: User[]
  console.log('Users:', users);
}

POST Request with Body

import { typedFetch, BadRequestError } from '@pbpeterson/typed-fetch';

const newUser = { name: 'John Doe', email: '[email protected]' };

const { response, error } = await typedFetch<User>('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(newUser),
});

if (error) {
  // Handle different error types
  if (error instanceof BadRequestError) {
    const validationErrors = await error.json();
    console.error('Validation failed:', validationErrors);
  } else {
    console.error('Request failed:', error.statusText);
  }
} else {
  const user = await response.json(); // Type: User
  console.log('Created user:', user);
}

Error Handling

HTTP Status Errors

typed-fetch provides specific error classes as individual exports for optimal tree-shaking:

import { typedFetch, NotFoundError, UnauthorizedError, BadRequestError, NetworkError } from '@pbpeterson/typed-fetch';

const { response, error } = await typedFetch<User>('/api/users/123');

if (error) {
  if (error instanceof NotFoundError) {
    console.log('User not found');
  } else if (error instanceof UnauthorizedError) {
    console.log('Please log in');
  } else if (error instanceof BadRequestError) {
    const details = await error.json();
    console.log('Invalid request:', details);
  } else if (error instanceof NetworkError) {
    console.log('Network error:', error.message);
  } else {
    console.log('Server error:', error.statusText);
  }
}

Available Error Classes

All errors are available as individual exports for optimal tree-shaking:

  • 4xx Client Errors:

    • BadRequestError (400)
    • UnauthorizedError (401)
    • PaymentRequiredError (402)
    • ForbiddenError (403)
    • NotFoundError (404)
    • MethodNotAllowedError (405)
    • NotAcceptableError (406)
    • ProxyAuthenticationRequiredError (407)
    • RequestTimeoutError (408)
    • ConflictError (409)
    • GoneError (410)
    • LengthRequiredError (411)
    • PreconditionFailedError (412)
    • RequestTooLongError (413)
    • RequestUriTooLongError (414)
    • UnsupportedMediaTypeError (415)
    • RequestedRangeNotSatisfiableError (416)
    • ExpectationFailedError (417)
    • ImATeapotError (418)
    • MisdirectedRequestError (421)
    • UnprocessableEntityError (422)
    • LockedError (423)
    • FailedDependencyError (424)
    • TooEarlyError (425)
    • UpgradeRequiredError (426)
    • PreconditionRequiredError (428)
    • TooManyRequestsError (429)
    • RequestHeaderFieldsTooLargeError (431)
    • UnavailableForLegalReasonsError (451)
  • 5xx Server Errors:

    • InternalServerError (500)
    • NotImplementedError (501)
    • BadGatewayError (502)
    • ServiceUnavailableError (503)
    • GatewayTimeoutError (504)
    • HttpVersionNotSupportedError (505)
    • VariantAlsoNegotiatesError (506)
    • InsufficientStorageError (507)
    • LoopDetectedError (508)
    • NotExtendedError (510)
    • NetworkAuthenticationRequiredError (511)
  • Network Errors:

    • NetworkError - For connection issues, timeouts, etc.

Specific Client Error Types

You can constrain the expected client errors (4xx) as a second generic parameter. Server errors (5xx) are always included since they can happen regardless of your input:

import { typedFetch, BadRequestError } from '@pbpeterson/typed-fetch';

// Specify expected client errors - server errors are automatically included
const { response, error } = await typedFetch<User, BadRequestError>('/api/users');

if (error) {
  // error is typed as: BadRequestError | ServerErrors | NetworkError
  // Server errors (5xx) are always included since you can't control them
  if (error instanceof BadRequestError) {
    const validationErrors = await error.json();
    console.log('Validation failed:', validationErrors);
  } else if (error.status >= 500) {
    console.log('Server error occurred:', error.statusText);
  }
}

// You can combine multiple client error types:
import { NotFoundError } from '@pbpeterson/typed-fetch';
type ExpectedErrors = NotFoundError | BadRequestError;
const { response, error } = await typedFetch<User, ExpectedErrors>('/api/users/123');
// error: NotFoundError | BadRequestError | ServerErrors | NetworkError

Advanced Usage

Error Response Bodies

All HTTP error classes provide access to the response body in multiple formats:

const { response, error } = await typedFetch<User>('/api/users', {
  method: 'POST',
  body: JSON.stringify(invalidData)
});

if (error) {
  // Access the error response body in different formats
  const errorJson = await error.json();        // Parse as JSON
  const errorText = await error.clone().text(); // Parse as text
  const errorBlob = await error.clone().blob(); // Parse as blob
  const errorBuffer = await error.clone().arrayBuffer(); // Parse as ArrayBuffer
  
  console.log('Server error details:', errorJson);
  
  // Access response headers
  const contentType = error.headers.get('content-type');
  
  // Access status information
  console.log(`Error ${error.status}: ${error.statusText}`);
}

Static Properties

Access status codes and text without creating instances:

import { NotFoundError, BadRequestError, InternalServerError } from '@pbpeterson/typed-fetch';

// Check status codes statically
if (response.status === NotFoundError.status) {
  console.log('Resource not found');
}

// All error classes have static properties
console.log(BadRequestError.status);     // 400
console.log(BadRequestError.statusText); // "Bad Request"
console.log(InternalServerError.status); // 500
console.log(InternalServerError.statusText); // "Internal Server Error"

Network vs HTTP Errors

import { typedFetch, NetworkError } from '@pbpeterson/typed-fetch';

const { response, error } = await typedFetch<User>('/api/users');

if (error) {
  if (error instanceof NetworkError) {
    console.log('Network issue - check connection');
  } else {
    // All other errors are HTTP errors with status property
    console.log(`HTTP error: ${error.status}`);
  }
}

Optional RequestInit

The second parameter is optional and defaults to an empty object:

// These are equivalent
await typedFetch<User[]>('/api/users');
await typedFetch<User[]>('/api/users', {});

API Reference

Exports

This library exports the main fetch function and individual error classes for optimal tree-shaking:

  • typedFetch - The main fetch function
  • Individual error classes: BadRequestError, NotFoundError, InternalServerError, etc.
// Import only what you need for optimal bundle size
import { typedFetch, NotFoundError, BadRequestError } from '@pbpeterson/typed-fetch';

// Or import all errors if needed
import * as Errors from '@pbpeterson/typed-fetch';

typedFetch<T, E>(url, options?)

Type Parameters:

  • T - The expected response body type
  • E extends ClientErrors - Specific client error class(es) to expect (optional, defaults to all client errors)

Parameters:

  • url: string - The URL to fetch
  • options: RequestInit - Fetch options (optional, defaults to {})

Returns:

Promise<{
  response: TypedResponse<T>;
  error: null;
} | {
  response: null;
  error: E | ServerErrors | NetworkError;
}>

Example with specific error types:

import { BadRequestError, NotFoundError } from '@pbpeterson/typed-fetch';

// Expect specific client errors - server errors always included
type ExpectedErrors = BadRequestError | NotFoundError;
const { response, error } = await typedFetch<User, ExpectedErrors>('/api/users/123');

// error will be: BadRequestError | NotFoundError | ServerErrors | NetworkError | null

Note: Server errors (5xx) are always included in the error union because they can occur regardless of your input validation or client-side logic.

Error Classes

All HTTP error classes extend BaseHttpError and include:

Instance Properties:

  • status: number - HTTP status code
  • statusText: string - HTTP status text
  • headers: Headers - Response headers

Instance Methods:

  • json(): Promise<any> - Parse error response body as JSON
  • text(): Promise<string> - Parse error response body as text
  • blob(): Promise<Blob> - Parse error response body as blob
  • arrayBuffer(): Promise<ArrayBuffer> - Parse error response body as ArrayBuffer
  • clone(): ErrorClass - Clone the error for multiple response body reads

Static Properties:

  • static status: number - HTTP status code (accessible without instantiation)
  • static statusText: string - HTTP status text (accessible without instantiation)

Inspiration

This library is inspired by Go's error handling philosophy where "errors are values." Instead of using exceptions for control flow, typed-fetch returns errors as regular values that you can inspect, handle, and pass around like any other data.

// Go pattern that inspired this library
result, err := http.Get("https://api.example.com/users")
if err != nil {
    // handle error
    return err
}
// use result
// typed-fetch equivalent
const { response, error } = await typedFetch<User[]>('/api/users');
if (error) {
    // handle error
    return error;
}
// use response

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.