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

@polygonlabs/verror

v1.0.1

Published

TypeScript-first, browser-friendly VError-inspired error handling library

Readme

@polygonlabs/verror

Structured error handling for TypeScript — cause chains, accumulated messages, and typed info fields that survive serialisation.

Zero runtime dependencies. Pure ESM. Works in Node.js and browsers.

Why not plain Error?

new Error('query failed') loses the context that caused it. You can attach a cause with new Error('query failed', { cause: dbErr }), but the cause message disappears when you log err.message, and JSON.stringify(err) produces {}.

VError solves both:

import { VError } from '@polygonlabs/verror';

const dbErr = new Error('connection refused');
const err = new VError('query failed', { cause: dbErr });

err.message;       // 'query failed: connection refused'
err.shortMessage;  // 'query failed'
JSON.stringify(err); // { name, message, shortMessage, cause: { ... }, info: {} }

The full cause chain accumulates into message at construction time. shortMessage gives you back the message you actually passed, without the appended chain.

Classes

VError

The base class. Wraps any Error as a cause and appends its full accumulated message.

const root = new Error('ECONNREFUSED');
const err = new VError('upstream timed out', { cause: root });
// err.message === 'upstream timed out: ECONNREFUSED'

Native Error.cause chains are walked automatically. If the cause itself has a .cause (ES2022 native cause), VError accumulates the whole chain — not just the immediate cause message. This matters because third-party libraries and Node.js built-ins increasingly set .cause directly:

const root = new Error('ECONNREFUSED');
const native = new Error('fetch failed', { cause: root });  // native ES2022 cause
const err = new VError('could not load config', { cause: native });

err.message; // 'could not load config: fetch failed: ECONNREFUSED'
//                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^
//                                       full native chain accumulated

serializeError and toJSON also follow native cause chains, so the full chain is preserved when serialising errors you did not create.

Structured context goes in info. Unlike the message, info fields are machine-readable and aggregate up the chain via VError.info():

const err1 = new VError('db error', { info: { table: 'users' } });
const err2 = new VError('request failed', { cause: err1, info: { requestId: 'abc' } });

VError.info(err2); // { table: 'users', requestId: 'abc' }
// Closer errors win on key conflicts.

WError (Wrapped Error)

Like VError but intentionally does not append the cause's message. Use it when you want to report a high-level outcome without the noise of the full cause chain in message:

const dbErr = new VError('query failed: connection refused');
const appErr = new WError('could not load user', { cause: dbErr });

appErr.message; // 'could not load user'  — cause message not appended
appErr.toString(); // 'WError: could not load user; caused by VError: query failed: ...'

The cause is still traversable — it just stays out of message. Use WError at service boundaries where the cause is an internal detail that shouldn't leak into user-facing strings.

MultiError

Wraps multiple concurrent failures:

import { MultiError, errorFromList } from '@polygonlabs/verror';

const results = await Promise.allSettled(tasks);
const errors = results.filter(r => r.status === 'rejected').map(r => r.reason);
const err = errorFromList(errors); // null | Error | MultiError

errorFromList returns null for empty, the single error for one, and a MultiError for many. errorForEach iterates either uniformly.

HTTP errors

HTTPError base class and 17 concrete subclasses, each with a typed statusCode:

import { NotFound, BadRequest, Forbidden } from '@polygonlabs/verror';

throw new NotFound('user not found', { cause: dbErr, info: { userId } });
// err.statusCode === 404
// err.toJSON() includes statusCode

All subclasses: BadRequest (400), NotAuthenticated (401), PaymentError (402), Forbidden (403), NotFound (404), MethodNotAllowed (405), NotAcceptable (406), Timeout (408), Conflict (409), Gone (410), LengthRequired (411), Unprocessable (422), TooManyRequests (429), GeneralError (500), NotImplemented (501), BadGateway (502), Unavailable (503).

Serialisation

JSON.stringify on a plain Error produces {} — the message and cause are lost. VError.toJSON() is called automatically by JSON.stringify and produces:

{
  "name": "VError",
  "message": "query failed: connection refused",
  "shortMessage": "query failed",
  "cause": { "name": "Error", "message": "connection refused", ... },
  "info": {}
}

To serialise an error you didn't create (e.g., from a catch block), use serializeError:

import { serializeError } from '@polygonlabs/verror';

try {
  await fetch(url);
} catch (err) {
  logger.error({ err: serializeError(err) }, 'fetch failed');
}

serializeError handles any value: returns undefined for non-Errors, delegates to toJSON() for VErrors, and produces the same JSON shape for plain Errors and native Error.cause chains.

Static helpers

All available as standalone imports or as static methods on VError:

import { cause, info, fullStack, findCauseByName, findCauseByType } from '@polygonlabs/verror';

| Helper | Purpose | |--------|---------| | cause(err) | Immediate cause, or null | | info(err) | Merged info from the full chain | | fullStack(err) | Stack trace with all chained causes appended | | findCauseByName(err, name) | First cause whose .name matches | | findCauseByType(err, Type) | First cause that is instanceof Type | | hasCauseWithName(err, name) | Boolean shorthand for findCauseByName | | hasCauseWithType(err, Type) | Boolean shorthand for findCauseByType | | errorFromList(errors) | null / single error / MultiError | | errorForEach(err, fn) | Iterate errors, unwrapping MultiError |

findCauseByName is useful even when you control the code. Once errors cross a serialisation boundary, or when multiple copies of this package exist in a dependency tree, instanceof checks fail. Name-based lookup works regardless.

Subclassing

Define the error name at the class level with override readonly name = '...' as const. String literals survive minification; new.target.name does not:

class DatabaseError extends VError {
  override readonly name = 'DatabaseError' as const;
}

class QueryTimeoutError extends DatabaseError {
  override readonly name = 'QueryTimeoutError' as const;
}

Unnamed subclasses inherit the parent's name — useful for internal specialisation without a new name at the error boundary:

class DatabaseError extends VError {
  override readonly name = 'DatabaseError' as const;
  constructor(message: string, options?: VErrorOptions) {
    super(message, { ...options, info: { ...options?.info, layer: 'db' } });
  }
}

Differences from the original verror

This package is inspired by verror and @openagenda/verror but diverges in several places. If you are migrating from either, here is what changed.

Constructor API

The original constructor puts options or cause first, message last, and supports printf-style format strings as additional arguments:

// original verror — multiple overloads
new VError(message)
new VError(options, message)
new VError(cause, message)
new VError(cause, 'failed to %s %d items', 'update', 42)

This package uses a single consistent signature — message first, options second:

// @polygonlabs/verror
new VError(message)
new VError(message, { cause, info })

No sprintf. Messages are plain strings. Printf-style formatting mixes presentation into the error layer, produces hard-to-test messages, and makes the TypeScript types awkward. Format at the call site if you need interpolation.

Name is a class-level declaration

The original allowed setting name per instance via the options object:

new VError({ name: 'DatabaseError' }, 'query failed')  // original only

This package does not support per-instance names. name is a property of the class, not the instance — set it with override readonly name:

class DatabaseError extends VError {
  override readonly name = 'DatabaseError' as const;
}

This makes the error type visible in TypeScript, survives minification (string literals are not mangled), and is consistent with how the rest of the JavaScript ecosystem defines error names.

WError requires a cause

In the original, WError can be constructed without a cause. Here, cause is required in the options — a WError with no cause is just a VError, so the constraint is enforced at the type level.

Native Error.cause chain accumulation

The original predates ES2022 Error.cause. When wrapping a native error that itself has a .cause, the original sees only the immediate cause message. This package walks the full native chain:

const root = new Error('ECONNREFUSED');
const native = new Error('upstream failed', { cause: root }); // ES2022 native cause
const err = new VError('request failed', { cause: native });

err.message; // 'request failed: upstream failed: ECONNREFUSED'
//                                                ^^^^^^^^^^^^ walked by this package
//                               ^^^^^^^^^^^^^^^ only this in original verror

WError still suppresses the entire cause chain from its own message, as expected.

Serialisation

The original has no built-in serialisation support — JSON.stringify(vErr) produces {} because Error properties are non-enumerable. This package implements toJSON() on all error classes and provides serializeError() for errors you did not create. The full cause chain is serialised recursively, including plain Error and native Error.cause links.

What was removed

  • sprintf / printf-style formatting — use template literals instead
  • name in constructor options — use override readonly name on the class
  • strict mode — the option existed in some forks; not present here
  • meta — appeared in some forks as a duplicate of info; not present here

What was added

  • serializeError() and toJSON() — full cause chain serialisation
  • Native Error.cause chain accumulation — walks ES2022 cause chains
  • HTTP error classesHTTPError and 17 typed subclasses
  • Pure ESM, browser-compatible — no require, no Node.js built-ins, zero runtime dependencies