# helpful-errors

> A standard set of errors and methods for simpler, safer, and easier to read code.

Latest version **1.7.5** (published 2026-07-19) · MIT license · 0 weekly downloads

## Install

```sh
npm install helpful-errors
pnpm add helpful-errors
yarn add helpful-errors
bun add helpful-errors
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; no vulnerabilities; has provenance; recently updated; high maintenance score; high quality score.

Warnings: low downloads; no esm support.

## Facts

| | |
|---|---|
| Version | 1.7.5 |
| Published | 2026-07-19 |
| First published | 2024-12-26 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | CommonJS |
| Node | >=8.0.0 |
| Dependencies | 1 |
| Unpacked size | 77.8 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 0 |
| Author | ehmpathy |
| Maintainers | uladkasach |
| Keywords | types, type guards, guards, type checks, checks, type, checking, typescript |

## Links

- npm: https://www.npmjs.com/package/helpful-errors
- Repository: https://github.com/ehmpathy/helpful-errors
- Issues: https://github.com/ehmpathy/helpful-errors/issues
- npm.io page: https://npm.io/package/helpful-errors

## Dependencies (1)

- [type-fns](https://npm.io/package/type-fns.md) 1.21.2

## Alternatives

- [@openai/codex-sdk](https://npm.io/package/@openai/codex-sdk.md) — 731.4K weekly downloads
- [babel-plugin-transform-react-jsx](https://npm.io/package/babel-plugin-transform-react-jsx.md) — 565.0K weekly downloads
- [babel-helper-remove-or-void](https://npm.io/package/babel-helper-remove-or-void.md) — 508.5K weekly downloads
- [@pnpm/store-controller-types](https://npm.io/package/@pnpm/store-controller-types.md) — 186.9K weekly downloads
- [react-native-signature-canvas](https://npm.io/package/react-native-signature-canvas.md) — 155.6K weekly downloads

## Recent versions

- 1.7.5 (latest) — 2026-07-19
- 1.7.3 — 2026-04-04
- 1.7.2 — 2026-03-18
- 1.7.1 — 2026-03-17
- 1.7.0 — 2026-01-31
- 1.5.3 — 2025-11-24
- 1.5.2 — 2025-11-24
- 1.5.1 — 2025-11-24
- 1.5.0 — 2025-11-24
- 1.4.0 — 2025-11-17
- 1.3.10 — 2025-09-01
- 1.3.9 — 2025-09-01
- 1.3.8 — 2024-12-26

## README

# helpful-errors

![test](https://github.com/ehmpathy/helpful-errors/workflows/test/badge.svg)
![publish](https://github.com/ehmpathy/helpful-errors/workflows/publish/badge.svg)

Standardized helpful errors and methods for simpler, safer, and easier to read code.

# Purpose

Standardize on helpful errors for simpler, safer, easier to read code
- extend the `HelpfulError` for observable and actionable error messages
- leverage the `UnexpectedCodePath` to eliminate complexity with narrowed codepaths
- leverage the `BadRequestError` to make it clear when your logic successfully rejected a request
- test that logic throws errors ergonomically with `getError`

# install

```sh
npm install --save helpful-errors
```

# use

### UnexpectedCodePathError

The `UnexpectedCodePath` error is probably the most common type of error you'll throw.

It's common in business logic that you'll face a scenario that is technically possible but logically shouldn't occur.

For example, lets say you're writing on-vehicle code to check the tire pressures of a vehicle.

```ts
// given the tires to check
const tires = Tire[];

// first get the tire pressures for each
const tirePressures: number[] = tires.map(tire => getTirePressure(tire));

// now get the lowest tire pressure
const lowestTirePressure: number | undefined = tires.sort()[0];
```

In this case, its technically possible that `lowestTirePressure` could be undefined: there could not be any tires.

However, this is definitely an unexpected code path for our application. We can just halt our logic if we reach here, since we dont need to solve for it.

```ts
// sanity check that we do have a tire pressure
if (lowestTirePressure === undefined)
  throw new UnexpectedCodePath('no tire pressures found. can not compute lowest tire pressure', { tires });
```

With this, the type of `lowestTirePressure` has been narrowed from `number | undefined` to just `number`, so you wont have any type errors anymore.

Further, if this case does occur in real life, then it will be really easy to debug what happened and why. Your error message will include the `tires` input that caused the problem making this a breeze to debug. No more `could not read property 'x' of undefined`!

### BadRequestError

The `BadRequestError` is probably the next most common type of error you'll throw.

It's common in business logic that callers will try to execute your logic with inputs that are simply logically not valid. The user may not understand that their input is not valid or there may just be a bug upstream that is resulting in invalid requests.

For example, imagine you have an api that returns the liked songs of a user
```ts
const getLikedSongsByUser = ({ userUuid }: { userUuid: string }) => {
  // lookup the user
  const user = await userDao.findByUuid({ uuid: userUuid });

  // if the user does not exist, this is an invalid request. we shouldn't be asked to lookup songs for fake users
  if (!user)
    throw new BadRequestError('user does not exist for uuid', { userUuid });

  // use a property of the user to lookup their favorite songs
  const songs = await spotifyApi.getLikesForUser({ spotifyUserId: user.spotifyUserId });
}
```

Whatever the reason for a caller to make a logically invalid request, it's important to distinguish when *your code* is at fault versus when *the request* is at fault.

This is particularly useful when you monitor error rates. Its important to distinguish whether your software `failed to execute` or whether it `successfully rejected` the request for observability in monitor dashboards. The `BadRequestError` enables us to do this easily

For example, libraries such as the [simple-lambda-handlers](https://github.com/ehmpathy/simple-lambda-handlers) leverage `BadRequestErrors` to ensure that a bad request both successfully returns an error to the caller but is not marked as an lambda invocation error.

### ConstraintError

The `ConstraintError` extends `BadRequestError` with a clearer, more intuitive name. Use it when the caller violated a constraint — invalid input, forbidden action, or broken business rule.

```ts
import { ConstraintError } from 'helpful-errors';

// guard clause with static throw
const phone = customer.phone ?? ConstraintError.throw('customer must have phone');

// validation
if (amount <= 0) throw new ConstraintError('amount must be positive', { amount });

// business rule
if (!user.canAccessResource(resource))
  throw new ConstraintError('user lacks permission', { userId: user.id, resourceId: resource.id });
```

`ConstraintError` includes:
- `ConstraintError.code.http` — `400` (same as BadRequestError)
- `ConstraintError.code.exit` — `2` (unix usage error convention)
- `ConstraintError.emoji` — `'✋'` (for log utilities)
- `instanceof BadRequestError` — `true` (backwards compatible)

### MalfunctionError

The `MalfunctionError` extends `UnexpectedCodePathError` with a clearer, more intuitive name. Use it when the system itself malfunctioned — a bug, unexpected state, or internal failure.

```ts
import { MalfunctionError } from 'helpful-errors';

// guard clause with static throw
const config = process.env.CONFIG ?? MalfunctionError.throw('config not loaded');

// impossible state detection
switch (status) {
  case 'active': return handleActive();
  case 'inactive': return handleInactive();
  default: throw new MalfunctionError('unknown status', { status });
}

// wrap external calls
const fetchUser = MalfunctionError.wrap(
  async (id: string) => api.getUser(id),
  { message: 'failed to fetch user', metadata: { service: 'user-api' } }
);
```

`MalfunctionError` includes:
- `MalfunctionError.code.http` — `500` (same as UnexpectedCodePathError)
- `MalfunctionError.code.exit` — `1` (unix general error convention)
- `MalfunctionError.emoji` — `'💥'` (for log utilities)
- `instanceof UnexpectedCodePathError` — `true` (backwards compatible)

### ConstraintError vs MalfunctionError

The fundamental distinction:

| error type | whose fault? | what it means | emoji |
|------------|--------------|---------------|-------|
| `ConstraintError` | caller's fault | "you can't do that" | ✋ |
| `MalfunctionError` | our fault | "we broke" | 💥 |

In logs, this makes debug instant:
```
✋ ConstraintError: customer must have a phone number
💥 MalfunctionError: payment processor returned unexpected shape
```

Both error types inherit all capabilities from their parent classes (`BadRequestError` and `UnexpectedCodePathError`). This includes `.throw()`, `.wrap()`, `.redact()`, and typed metadata generics.

### HelpfulError

The `HelpfulError` is the backbone of this pattern and is what you'll `extend` whenever you want to create a custom error.

The purpose of this error is to be as helpful as possible to whoever has to read it when its thrown.

To fulfill this goal, the error makes it very easy to specify what the issue was as well as any other information that may be helpful to understanding why it occurred at the time. It then pretty prints this information to make it easy to read when observing.

```ts
throw new HelpfulError(
  'the message of the error goes here',
  {
    context,
    relevantInfo,
    potentiallyHelpfulVariables,
    goHere,
  }
)
```

#### .metadata generic

You can define typed metadata for your custom error classes via the generic type parameter:

```ts
class UserError extends HelpfulError<{ userId: string; action: string }> {}

const error = new UserError('operation failed', {
  userId: '123',
  action: 'delete'
});

// typed access to metadata
error.metadata.userId;  // string
error.metadata.action;  // string
error.metadata.wrong;   // typescript error
```

#### .metadata getter

Access the original metadata object via the `.metadata` getter:

```ts
const error = new HelpfulError('failed', {
  userId: '123',
  context: { action: 'delete' }
});

// access original metadata (not the formatted message)
console.log(error.metadata);
// { userId: '123', context: { action: 'delete' } }

// useful for programmatic access
if (error.metadata.userId) {
  trackErrorByUser(error.metadata.userId);
}
```

Note: the `.metadata` property is non-enumerable, so it won't appear in `Object.keys()` or `JSON.stringify()` output — the metadata is already serialized in the message.

#### environment variables

Control error message format via `ERROR_EXPAND`:

```sh
# default: pretty-printed json (multi-line)
ERROR_EXPAND=true

# compact single-line json
ERROR_EXPAND=false
```

### getError

The `getError` method is the cherry-on-top of this library.

When you write tests for logic that throws an error in certain situations, you may want to verify that the code indeed throws this error in a test.

The `getError` utility makes it really easy to assert that the expected error is thrown.

Under the hood, it executes or awaits the logic or promise you give it as input, catches the error that is thrown, or returns a `NoErrorThrownError` if no error occurred. It does the legwork of all three cases you may need to use it in and defines the return type correctly.

usecase 1: synchronous logic
```ts
const doSomething = () => { throw new HelpfulError('found me'); }

const error = getError(() => doSomething())
expect(error).toBeInstanceOf(HelpfulError);
expect(error.message).toContain('found me')
```

usecase 2: asynchronous logic
```ts
const doSomething = async () => { throw new HelpfulError('found me'); }

const error = await getError(() => doSomething())
expect(error).toBeInstanceOf(HelpfulError);
expect(error.message).toContain('found me')
```

usecase 3: a promise
```ts
const doSomething = async () => { throw new HelpfulError('found me'); }

const error = await getError(doSomething())
expect(error).toBeInstanceOf(HelpfulError);
expect(error.message).toContain('found me')
```

### .throw

The errors extended from the `HelpfulError` include a `.throw` static method for convenient usage with ternaries or condition chains

For example, instead of
```ts
const phone = customer.phoneNumber ?? (() => {
  throw new UnexpectedCodePathError(
    'customer has relationship without phone number. how is that possible?',
    { customer },
  );
})();
```

You can simply write
```ts
const phone = customer.phoneNumber ?? UnexpectedCodePathError.throw(
  'customer does not have a phone. how is that possible?',
  { customer },
);
```

### .wrap

The errors extended from `HelpfulError` include a `.wrap` static method for wrapping functions with helpful error handling. This provides a cleaner alternative to try-catch blocks while automatically preserving error context.

For example, instead of:
```ts
const getUser = async (id: string) => {
  try {
    return await database.query('SELECT * FROM users WHERE id = ?', [id]);
  } catch (error) {
    throw new HelpfulError('could not get user', {
      userId: id,
      cause: error,
    });
  }
};
```

You can simply write:
```ts
const getUser = HelpfulError.wrap(
  async (id: string) => {
    return await database.query('SELECT * FROM users WHERE id = ?', [id]);
  },
  {
    message: 'could not get user',
    metadata: { table: 'users' },
  }
);
```

The `.wrap` method works with both synchronous and asynchronous functions, and automatically uses the correct error variant:

```ts
// Works with custom error variants
const validateEmail = BadRequestError.wrap(
  (email: string) => {
    if (!email.includes('@')) throw new Error('invalid format');
    return email;
  },
  {
    message: 'email validation failed',
    metadata: { field: 'email' },
  }
);

// Works with async functions
const processPayment = HelpfulError.wrap(
  async (amount: number) => {
    return await paymentGateway.charge(amount);
  },
  {
    message: 'could not process payment',
    metadata: { service: 'stripe' },
  }
);
```

### .redact

The errors extended from `HelpfulError` include a `.redact` method for creating redacted clones of errors. This is useful when you need to prevent internal implementation details from leaking to frontends or external systems.

The `.redact` method accepts an array specifying which parts to redact: `['metadata']`, `['cause']`, or both `['metadata', 'cause']`.

```ts
// imagine you have an error with some internal details you'd like to keep private
const error = new HelpfulError('failed to fetch user profile', {
  query: 'SELECT * FROM users WHERE id = ?',
  params: {
    userId: 'usr_123',
  },
  cause: new Error('ECONNREFUSED: connection timeout'),
});

// you can redact both metadata and cause, to make it safe to expose
const redactedForFrontend = error.redact(['metadata', 'cause']);
console.log(redactedForFrontend.message); // "failed to fetch user profile"
console.log(redactedForFrontend.cause); // undefined
```

### HelpfulError parameter options.cause

The .cause parameter is a helpful feature of native errors. It allows you to chain errors together in a way that retains the full stack trace across errors.

For example, sometimes, the original error that your code experiences can be reworded to make it easier to debug. By using the .cause option, you're able to retain the stack trace and reference of the original error while throwing a new, more helpful, error.

```ts
// imagine you're using some api which throws an unhelpful error
const apiGetS3Object = async (input: { key: string }) => { throw new Error("no access") }

// you can catch and extend the error to add more context
const helpfulGetS3Object = async (input: { key: string }) => {
  try {
    await getS3Object();
  } catch (error) {
    if (error.message === "no access") throw HelpfulError("getS3Object.error: could not get object", {
      cause: error, // !: by adding the "cause" here, we'll retain the stack trace of the original error
      input,
    })
  }
}
```

### .toJSON

HelpfulError includes a custom `.toJSON()` method for helpful serialization.

By default, errors omit `message` and `stack` from serialization. Helpful errors, instead, explicitly includes them to save dev's hours via clear errors that surface full context:

```ts
const error = new HelpfulError('failed', { userId: '123' });

JSON.stringify(error);
// {
//   "name": "HelpfulError",
//   "message": "failed, { \"userId\": \"123\" }",
//   "stack": "..."
// }

// useful for api responses
res.status(500).json({ error: error.toJSON() });
```

### .code

Errors extended from `HelpfulError` support declarative error codes via the `.code` property. This enables machine-readable error classification for api responses, logs, and metrics.

#### default codes

The built-in error classes have default http codes:
- `BadRequestError` has `{ http: 400 }`
- `UnexpectedCodePathError` has `{ http: 500 }`
- `HelpfulError` has no default code

```ts
const error = new BadRequestError('invalid input');
console.log(error.code?.http); // 400
console.log(error.code?.slug); // undefined
```

#### custom error classes with baked-in codes

Define error classes with baked-in codes for consistent classification:

```ts
class DeclinedPaymentError extends BadRequestError {
  public static code = { http: 402, slug: 'DECLINED:PAYMENT' } as const;
}

const error = new DeclinedPaymentError('card rejected');
console.log(error.code); // { http: 402, slug: 'DECLINED:PAYMENT' }
```

#### instance-level code override

Supply a code at throw time for context-specific classification:

```ts
throw new BadRequestError('email already registered', {
  code: { slug: 'DUPLICATE_EMAIL' },
  email,
});

// error.code => { http: 400, slug: 'DUPLICATE_EMAIL' }
```

Instance codes merge with class codes — instance fields override class fields:

```ts
throw new BadRequestError('validation failed', {
  code: { http: 422, slug: 'VALIDATION' },
});

// error.code => { http: 422, slug: 'VALIDATION' }
```

#### opt-out with code: null

Explicitly clear any inherited code:

```ts
const error = new DeclinedPaymentError('card rejected', { code: null });
console.log(error.code); // undefined
```

#### serialization

Error codes only appear in JSON serialization when a slug is present — this prevents log spam with default http codes and ensures opt-in only behavior:

```ts
// no slug => code omitted from JSON
const error1 = new BadRequestError('test');
JSON.stringify(error1); // no "code" field

// slug present => code included in JSON
const error2 = new BadRequestError('test', { code: { slug: 'CUSTOM' } });
JSON.stringify(error2); // includes { "code": { "http": 400, "slug": "CUSTOM" } }
```

#### api handler example

```ts
app.use((err, req, res, next) => {
  const code = err.code ?? { http: 500 };
  res.status(code.http ?? 500).json({
    error: err.message,
    code: code.slug,
  });
});
```

### isHelpfulError

The `isHelpfulError` guard answers "is this one of ours?" — even across duplicate copies of `helpful-errors` in a dependency tree, where `instanceof` silently fails.

When two copies of the package are installed (e.g. a nested dependency has its own copy), each copy defines its own distinct classes. An error thrown from one copy is **not** an `instanceof` the `HelpfulError` of the other copy — so `error instanceof ConstraintError` returns `false` across that boundary, and error handlers misclassify.

`isHelpfulError` sidesteps this: every `HelpfulError` is stamped, in its base constructor, with a realm-global brand (`Symbol.for('helpful-errors')`). Because the brand lives in the shared symbol registry — not on any one copy's class — the guard returns `true` regardless of which copy minted the error. The brand is also minification-proof (it keys off a registered symbol, not a class name).

```ts
import { isHelpfulError } from 'helpful-errors';

// http error middleware — classify correctly across copy boundaries
app.use((err, req, res, next) => {
  if (isHelpfulError(err)) {
    return res.status(err.code?.http ?? 500).json(err.toJSON());
  }
  return res.status(500).json({ message: 'internal error' });
});
```

It accepts `unknown`, so a `catch (e: unknown)` needs no pre-cast, and narrows to `HelpfulError` on success:

```ts
try {
  await runTask();
} catch (err: unknown) {
  if (isHelpfulError(err)) {
    // err is narrowed to HelpfulError — .code, .metadata, .redact() are all safe
    console.log(err.code);
  }
}
```

#### version gate

Each copy stamps its own package version onto the brand. Pass a minimum version to assert "the copy that minted this error was at least version X" — useful to gate on a capability added in a specific release:

```ts
// true only if branded AND the stamped version is >= '1.8.0'
if (isHelpfulError(err, { version: { min: '1.8.0' } })) {
  // safe to rely on behavior introduced at 1.8.0
}
```

The compare is semver-aware (numeric `major.minor.patch`, not lexicographic). A non-branded value, or a stamped version below `min`, returns `false` — the guard never throws.

### getHelpfulErrorVersion

Read the stamped version off any branded error, or `undefined` for a non-branded value:

```ts
import { getHelpfulErrorVersion } from 'helpful-errors';

getHelpfulErrorVersion(new HelpfulError('boom')); // e.g. '1.8.0'
getHelpfulErrorVersion(new Error('plain'));       // undefined
getHelpfulErrorVersion(null);                     // undefined
```

#### scope + caveats

- **in-process only**: "global" means realm-global within one process. The brand does not survive JSON serialize/revive (a revived error is a plain object with no live brand) or cross-realm transfer (`worker_threads`, `vm`), so `isHelpfulError` returns `false` for those.
- **version floor for detection**: a copy of `helpful-errors` older than the release that introduced the brand never stamps a brand, so `isHelpfulError` cannot detect errors it mints. Cross-copy detection requires all copies to be on a version at or above the one that introduced the brand.

---
_Source: https://npm.io/package/helpful-errors · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
