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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pavelpotemkin/utils

v2.0.0

Published

Result type and an axios-based HTTP client that never throws

Readme

@pavelpotemkin/utils

Result type and an axios-based HTTP client that never throws.

  • Result<T, E> — { ok: true, value } | { ok: false, error }, same shape and names as @pavelpotemkin/portals-market-api-sdk 2.x.
  • HttpClient — every method returns Result<T, HttpError>; timeouts, AbortSignal, response schemas.
  • One error class HttpError with kind.
  • Schemas are structural: zod 3, zod 4 or anything with safeParse. zod is not a dependency.
  • Peer dependency: axios@^1.20.0. ESM + CJS, sideEffects: false.

Upgrading from 1.x: see CHANGELOG.md.

Install

npm install @pavelpotemkin/utils axios

Result

import { ok, err, isOk, isErr, unwrap, type Result } from "@pavelpotemkin/utils";

const parse = (s: string): Result<number> => {
  const n = Number(s);
  return Number.isNaN(n) ? err(new Error(`not a number: ${s}`)) : ok(n);
};

const r = parse("42");
if (isOk(r)) r.value; // number
if (isErr(r)) r.error; // Error

unwrap(ok(1)); // 1
unwrap(err(new Error("x"))); // throws that error

Result<T, E = Error>, types Ok<T> and Err<E>. ok() returns Ok<T>, err() returns Err<E>; both are assignable to any Result with a compatible T/E.

HttpClient

import { HttpClient, isHttpError } from "@pavelpotemkin/utils";
import { z } from "zod";

const client = new HttpClient({
  baseURL: "https://api.example.com/api",
  headers: { "Content-Type": "application/json" },
  timeoutMs: 20_000, // default 0 — no timeout
});

// axios stays public: interceptors, defaults.adapter (mocks), direct calls.
client.axios.interceptors.request.use((config) => {
  config.headers.set("Authorization", `tma ${token}`);
  return config;
});

const User = z.object({ id: z.number(), name: z.string() });

const res = await client.get("/users/1", { schema: User }); // Result<{ id: number; name: string }, HttpError>
if (res.ok) {
  console.log(res.value.name);
} else if (isHttpError(res.error, "http") && res.error.status === 404) {
  // not found
} else {
  console.error(res.error.kind, res.error.message);
}

Methods: get(path, options), post(path, body, options), put, patch, delete, and request(method, path, body, options). Bodies are sent as JSON by axios.

Request options:

| Option | Meaning | | --- | --- | | params | Query. Arrays are joined with , (ids=1,2); undefined, null and empty arrays are skipped | | schema | Response schema with safeParse (zod 3/4); the result type is inferred from it | | headers | Request headers | | signal | AbortSignal → kind: "aborted" | | timeoutMs | Overrides the client timeoutMs; 0 — none → kind: "timeout" | | responseType | "json" (default) or "text" (raw string as the value) |

Timeouts and cancellation work even with custom adapters that ignore signal: the client stops waiting and also aborts the axios request. Only AbortController and setTimeout are used (no AbortSignal.timeout/any), so older WebViews are fine.

The body is requested as text and parsed by the client: an empty body (204) gives undefined, a non-JSON 2xx body is kind: "parse". If an adapter returns an object (mock adapters), it is taken as the parsed body.

HttpError

class HttpError extends Error {
  name: "HttpError";
  kind: "http" | "network" | "timeout" | "aborted" | "parse" | "validation";
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
  endpoint: string;     // path as passed to the client
  status?: number;      // http, parse, validation
  code?: string;        // `code` from a JSON error body
  data?: unknown;       // parsed body; for validation — the data that failed the schema
  body?: string;        // raw text when the adapter returned text
  issues?: ValidationIssue[]; // validation
  retryable: boolean;
  cause?: unknown;      // network/timeout/aborted: the original error
  isBadRequest; isUnauthorized; isNotFound; isRateLimited; isServerError; // only for kind "http"
}

| kind | When | | --- | --- | | http | Non-2xx response | | network | No response: connection failed, the adapter or an interceptor threw | | timeout | timeoutMs or axios timeout elapsed | | aborted | Caller's signal fired or axios canceled the request | | parse | 2xx body is not JSON | | validation | 2xx JSON did not pass schema |

message: "<METHOD> <endpoint>: HTTP <status>: <body.message | statusText>" for http, a similar "<METHOD> <endpoint>: …" line for the other kinds.

retryable: true for 429 (any method) and for 408/500/502/503/504, network, timeout on GET, PUT, DELETE. POST/PATCH after a lost response are not retryable: reconcile the outcome instead.

isHttpError(error, ...kinds) is a type guard; with "http" it narrows status to number, with "validation" — issues to an array:

if (isHttpError(error, "http") && error.status >= 500) …
if (isHttpError(error, "network", "timeout")) …

The constructor is public for tests and mocks; message, code and retryable have defaults:

new HttpError({ kind: "http", method: "POST", endpoint: "/bet", status: 422, data: { code: "LIMIT" } });

Types

import type { Brand, ValidationSchema, ValidationIssue } from "@pavelpotemkin/utils";

type UserId = Brand<string, "UserId">;