<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/http-errors-vs-hapi-boom-vs-http-status-codes-http-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/http-errors-vs-hapi-boom-vs-http-status-codes-http-2026/raw.md -->
<!-- Source path: content/guides/http-errors-vs-hapi-boom-vs-http-status-codes-http-2026.mdx -->

---
og_image: "/images/guides/http-errors-vs-hapi-boom-vs-http-status-codes-http-2026.webp"
title: "http-errors vs @hapi/boom vs http-status-codes 2026"
description: "Compare http-errors, @hapi/boom, and http-status-codes for HTTP error handling in Node.js. Error creation, status codes, error responses, and which error."
date: "2026-03-09"
author: "PkgPulse Team"
tags: ["nodejs", "typescript", "developer-tools", "api"]
---

## TL;DR

**http-errors** is the standard HTTP error factory — creates error objects with status codes, used by Express and Koa internally. **@hapi/boom** is the Hapi framework's error utility — rich error objects with data payloads, error decoration, and consistent error response format. **http-status-codes** is a TypeScript enum/constant library for HTTP status codes — type-safe status code references, reason phrases, no error object creation. In 2026: http-errors for Express/Koa middleware, @hapi/boom for structured API error responses, http-status-codes for type-safe status constants.

## Key Takeaways

- **http-errors**: ~30M weekly downloads — Express/Koa core, creates Error objects with statusCode
- **@hapi/boom**: ~5M weekly downloads — Hapi ecosystem, rich error payloads, consistent format
- **http-status-codes**: ~3M weekly downloads — TypeScript enums for status codes, zero error creation
- http-errors and @hapi/boom create error objects; http-status-codes is just constants
- http-errors is the simplest — `createError(404, "Not found")`
- @hapi/boom provides the richest error format with data, headers, and decoration

---

## http-errors

[http-errors](https://github.com/jshttp/http-errors) — standard HTTP error factory:

### Basic usage

```typescript
import createError from "http-errors"

// Create errors by status code:
throw createError(404, "Package not found")
// → { status: 404, statusCode: 404, message: "Package not found", expose: true }

throw createError(500, "Database connection failed")
// → { status: 500, statusCode: 500, message: "Database connection failed", expose: false }

// Named constructors:
throw new createError.NotFound("Package not found")
throw new createError.BadRequest("Invalid package name")
throw new createError.Unauthorized("Authentication required")
throw new createError.Forbidden("Insufficient permissions")
throw new createError.InternalServerError("Something went wrong")
```

### Express middleware

```typescript
import express from "express"
import createError from "http-errors"

const app = express()

app.get("/api/packages/:name", async (req, res, next) => {
  try {
    const pkg = await db.findPackage(req.params.name)
    if (!pkg) {
      throw createError(404, `Package "${req.params.name}" not found`)
    }
    res.json(pkg)
  } catch (err) {
    next(err)
  }
})

// Error handler:
app.use((err, req, res, next) => {
  if (createError.isHttpError(err)) {
    res.status(err.statusCode).json({
      error: err.message,
      statusCode: err.statusCode,
    })
  } else {
    res.status(500).json({ error: "Internal server error" })
  }
})
```

### Error properties

```typescript
import createError from "http-errors"

// Add custom properties:
const error = createError(422, "Validation failed", {
  fields: { name: "Required", version: "Must be semver" },
})
// error.fields → { name: "Required", version: "Must be semver" }

// expose property — controls if message is shown to client:
// 4xx errors: expose = true (client errors, safe to show)
// 5xx errors: expose = false (server errors, hide details)

const clientError = createError(400, "Bad request")
clientError.expose  // true

const serverError = createError(500, "Database crashed")
serverError.expose  // false

// Check if an error is an HTTP error:
createError.isHttpError(clientError)  // true
createError.isHttpError(new Error())  // false
```

### With Koa

```typescript
import Koa from "koa"
import createError from "http-errors"

const app = new Koa()

app.use(async (ctx) => {
  const pkg = await db.findPackage(ctx.params.name)
  if (!pkg) {
    // Koa natively understands http-errors:
    ctx.throw(404, "Package not found")
    // Equivalent to: throw createError(404, "Package not found")
  }
  ctx.body = pkg
})
```

---

## @hapi/boom

[@hapi/boom](https://github.com/hapijs/boom) — rich HTTP error objects:

### Basic usage

```typescript
import Boom from "@hapi/boom"

// Named error factories:
throw Boom.notFound("Package not found")
// → { statusCode: 404, error: "Not Found", message: "Package not found" }

throw Boom.badRequest("Invalid package name")
throw Boom.unauthorized("Invalid token")
throw Boom.forbidden("Admin access required")
throw Boom.conflict("Package already exists")
throw Boom.tooManyRequests("Rate limit exceeded")
```

### Error with data payload

```typescript
import Boom from "@hapi/boom"

// Attach data to errors:
throw Boom.badRequest("Validation failed", {
  fields: [
    { field: "name", message: "Required" },
    { field: "version", message: "Must be valid semver" },
  ],
})
// Output:
// {
//   statusCode: 400,
//   error: "Bad Request",
//   message: "Validation failed",
//   data: { fields: [...] }
// }

// The output() method gives a consistent response format:
const err = Boom.notFound("Package not found", { name: "nonexist" })
const response = err.output
// response.statusCode → 404
// response.payload → { statusCode: 404, error: "Not Found", message: "Package not found" }
// response.headers → {}
```

### Custom headers

```typescript
import Boom from "@hapi/boom"

// Rate limiting error with headers:
const error = Boom.tooManyRequests("Rate limit exceeded")
error.output.headers["Retry-After"] = "60"
error.output.headers["X-RateLimit-Limit"] = "100"
error.output.headers["X-RateLimit-Remaining"] = "0"

// Authentication error with WWW-Authenticate:
const authError = Boom.unauthorized("Invalid token", "Bearer", {
  realm: "api",
  error: "invalid_token",
})
// Automatically sets: WWW-Authenticate: Bearer realm="api", error="invalid_token"
```

### Error decoration

```typescript
import Boom from "@hapi/boom"

// Decorate existing errors:
try {
  await db.query("SELECT * FROM packages")
} catch (dbError) {
  // Wrap database error as HTTP error:
  throw Boom.internal("Database query failed", dbError)
  // Original error preserved in err.data
}

// Check if error is a Boom error:
Boom.isBoom(error)  // true
Boom.isBoom(new Error())  // false

// Convert any error to Boom:
const regularError = new Error("Something broke")
const boomError = Boom.boomify(regularError, { statusCode: 502 })
```

### Express integration

```typescript
import express from "express"
import Boom from "@hapi/boom"

const app = express()

app.get("/api/packages/:name", async (req, res, next) => {
  try {
    const pkg = await db.findPackage(req.params.name)
    if (!pkg) throw Boom.notFound(`Package "${req.params.name}" not found`)
    res.json(pkg)
  } catch (err) {
    next(err)
  }
})

// Boom error handler:
app.use((err, req, res, next) => {
  if (Boom.isBoom(err)) {
    const { output } = err
    // Set custom headers:
    Object.entries(output.headers).forEach(([key, value]) => {
      res.setHeader(key, value)
    })
    res.status(output.statusCode).json(output.payload)
  } else {
    res.status(500).json({ error: "Internal server error" })
  }
})
```

---

## http-status-codes

[http-status-codes](https://github.com/prettymuchbryce/http-status-codes) — status code constants:

### Status code enums

```typescript
import { StatusCodes, ReasonPhrases, getReasonPhrase } from "http-status-codes"

// Type-safe status codes:
StatusCodes.OK                    // 200
StatusCodes.CREATED               // 201
StatusCodes.BAD_REQUEST           // 400
StatusCodes.UNAUTHORIZED          // 401
StatusCodes.NOT_FOUND             // 404
StatusCodes.INTERNAL_SERVER_ERROR // 500
StatusCodes.TOO_MANY_REQUESTS    // 429

// Reason phrases:
ReasonPhrases.OK                    // "OK"
ReasonPhrases.NOT_FOUND             // "Not Found"
ReasonPhrases.INTERNAL_SERVER_ERROR // "Internal Server Error"

// Get phrase from code:
getReasonPhrase(404)  // "Not Found"
getReasonPhrase(200)  // "OK"
```

### With Express

```typescript
import express from "express"
import { StatusCodes, ReasonPhrases } from "http-status-codes"

const app = express()

app.get("/api/packages/:name", async (req, res) => {
  const pkg = await db.findPackage(req.params.name)

  if (!pkg) {
    return res.status(StatusCodes.NOT_FOUND).json({
      error: ReasonPhrases.NOT_FOUND,
      message: `Package "${req.params.name}" not found`,
    })
  }

  res.status(StatusCodes.OK).json(pkg)
})

app.post("/api/packages", async (req, res) => {
  const pkg = await db.createPackage(req.body)
  res.status(StatusCodes.CREATED).json(pkg)
})
```

### Type-safe responses

```typescript
import { StatusCodes, getReasonPhrase, getStatusCode } from "http-status-codes"

// Convert between codes and phrases:
getReasonPhrase(StatusCodes.NOT_FOUND)  // "Not Found"
getStatusCode("Not Found")              // 404

// Type checking:
function sendError(res: Response, code: StatusCodes, message: string) {
  res.status(code).json({
    statusCode: code,
    error: getReasonPhrase(code),
    message,
  })
}

sendError(res, StatusCodes.BAD_REQUEST, "Invalid input")
sendError(res, StatusCodes.UNAUTHORIZED, "Token expired")
```

### Combining with http-errors

```typescript
import createError from "http-errors"
import { StatusCodes } from "http-status-codes"

// Use status codes constants with http-errors:
throw createError(StatusCodes.NOT_FOUND, "Package not found")
throw createError(StatusCodes.BAD_REQUEST, "Invalid name")
throw createError(StatusCodes.CONFLICT, "Already exists")

// Better than magic numbers:
// throw createError(404, "Not found")  ← what's 404?
// throw createError(StatusCodes.NOT_FOUND, "Not found")  ← clear!
```

---

## Feature Comparison

| Feature | http-errors | @hapi/boom | http-status-codes |
|---------|-----------|-----------|------------------|
| Creates error objects | ✅ | ✅ | ❌ (constants only) |
| Named constructors | ✅ | ✅ | ❌ |
| Data payload | ⚠️ (custom props) | ✅ (structured) | ❌ |
| Custom headers | ❌ | ✅ | ❌ |
| Error wrapping | ❌ | ✅ (boomify) | ❌ |
| Status code enums | ❌ | ❌ | ✅ |
| Reason phrases | ❌ | ✅ (in output) | ✅ |
| expose flag | ✅ | ❌ (all exposed) | ❌ |
| TypeScript | ✅ | ✅ | ✅ |
| Used by | Express, Koa | Hapi | Any framework |
| Weekly downloads | ~30M | ~5M | ~3M |

---

## When to Use Each

**Use http-errors if:**
- Using Express or Koa (they understand http-errors natively)
- Want simple error creation with status codes
- Need the expose flag (hide 5xx details from clients)
- Want the most lightweight error factory

**Use @hapi/boom if:**
- Need structured error responses with data payloads
- Want custom headers on error responses (rate limiting, auth)
- Building APIs that need consistent error format
- Need to wrap/decorate existing errors

**Use http-status-codes if:**
- Want type-safe status code constants (no magic numbers)
- Need reason phrase lookups
- Combining with other error libraries (http-errors + status constants)
- Want IDE autocomplete for HTTP status codes

---

## Building a Consistent Error Response Format

One of the most common pain points in Node.js API development is inconsistent error response shapes. A 404 from one endpoint returns `{ message: "Not found" }`, another returns `{ error: "Package not found" }`, and a validation error returns a flat string. Clients end up with defensive parsing code to handle every possible error shape. Both http-errors and @hapi/boom solve this differently.

http-errors doesn't enforce a response shape — it creates error objects with `statusCode` and `message`, but your error-handling middleware decides how to serialize them to JSON. This gives maximum flexibility but means each team invents their own convention. The most common pattern is a middleware that checks `createError.isHttpError(err)` and serializes `{ statusCode: err.statusCode, message: err.expose ? err.message : "Internal server error" }`. The `expose` flag is the key detail: 4xx errors expose their message (it's safe to tell the client "Package not found"), while 5xx errors suppress the message to avoid leaking internal implementation details.

@hapi/boom enforces a specific response format via `err.output.payload`: always `{ statusCode: number, error: string, message: string }`. The `error` field contains the HTTP reason phrase ("Not Found", "Bad Request"), `message` is your custom message, and `statusCode` is the numeric code. This consistent shape means your API clients can write a single error parser that handles every error response. For APIs with multiple consumers (mobile apps, third-party integrations, internal services), the predictable structure reduces integration friction. Adding `data` to a Boom error puts it in `err.output.payload.data` when you call `err.reformat(true)`.

## Framework Error Handling Patterns with Hono and Fastify

Express and Koa are the traditional targets for http-errors, but the Node.js framework landscape in 2026 includes Hono, Fastify, and Elysia. http-errors works with any framework that supports error-passing middleware, but the integration ergonomics vary.

Fastify has its own error handling system built around `fastify.setErrorHandler()` and throws HTTP errors using Fastify's `reply.internalServerError()` / `reply.notFound()` methods. These produce Fastify's native error format, not http-errors objects. You can still use http-errors in Fastify by throwing them inside route handlers and catching them in a custom error handler, but it adds a translation layer. @hapi/boom is similarly framework-specific — it was designed for the Hapi ecosystem and works best there.

Hono has a built-in `HTTPException` class (`throw new HTTPException(404, { message: "Not found" })`) that serves the same purpose as http-errors without the dependency. For projects using Hono exclusively, http-errors adds little value. The http-status-codes library is the most portable of the three precisely because it contains no error objects — `StatusCodes.NOT_FOUND` is just the number `404`, which works in any framework's error-throwing mechanism.

## Using http-status-codes for Type-Safe API Client Code

http-status-codes isn't only useful in server-side handlers — it's equally valuable in API client code, test assertions, and middleware checks. When writing tests that verify error responses, `expect(response.status).toBe(StatusCodes.UNPROCESSABLE_ENTITY)` is clearer than `expect(response.status).toBe(422)`. The numeric code `422` is meaningful to HTTP veterans but opaque to developers unfamiliar with the full status code space; the named constant is self-documenting.

For API client libraries that parse response status codes, the `isClientError()` and `isServerError()` utility functions (checking if a code falls in the 4xx or 5xx range respectively) simplify response classification without hardcoding magic number comparisons like `status >= 400 && status < 500`. The library is 3KB minified, has zero dependencies, and ships with both CommonJS and ESM builds, making it safe to include in browser-side code alongside server-side usage.

---

## Internationalization of Error Messages

Production APIs that serve international audiences often need to return error messages in the user's language. None of the three libraries handles i18n natively — error message strings are hardcoded English in all three. The integration pattern for multilingual error messages varies by library. With http-errors, the standard approach is to pass a message key rather than a human-readable string in development, then resolve the key to a localized message in the error-handling middleware using `req.acceptsLanguages()` and an i18n library like `i18next`. The `expose` flag still works correctly — 4xx error keys are exposed to the client for translation lookup, while 5xx keys are suppressed.

@hapi/boom's consistent output format makes i18n slightly cleaner: the `message` field in `output.payload` can hold a translation key (`"errors.packageNotFound"`) that clients translate client-side, or the error handler can resolve it server-side before sending the response. The `data` field is particularly useful for error-specific context — a 422 Unprocessable Entity error for form validation can include `data: { field: "name", key: "validation.required" }` alongside the generic message, giving the client everything it needs to display a localized, field-specific error without additional API calls.

## Error Serialization for API Documentation and OpenAPI

Well-structured HTTP errors are directly related to the quality of your OpenAPI documentation. When your error handling is consistent — always returning `{ statusCode, error, message }` for 4xx errors — your OpenAPI spec can accurately document the response schema for each status code. @hapi/boom is the easiest to document in OpenAPI because its `output.payload` structure is fixed: any team or tool that generates OpenAPI specs from runtime behavior can reliably infer the error shape without special-casing individual endpoints.

http-errors is more challenging to document precisely because the response shape is whatever your error-handling middleware produces. Teams typically define a standard error response type in TypeScript — `interface ApiError { statusCode: number; message: string; error?: string }` — and ensure the error handler serializes http-errors to that shape. The TypeScript type serves as the documentation contract: OpenAPI generators that read your route handler return types can infer the error response from the declared type rather than from the runtime error object.

http-status-codes is uniquely useful in the context of OpenAPI generation tools. Libraries like `tsoa` and `@nestjs/swagger` generate OpenAPI specs from TypeScript decorators and return types. When your handler declares `@Response<ApiError>(StatusCodes.NOT_FOUND, "Package not found")`, the generator uses the `StatusCodes.NOT_FOUND` constant (404) to populate the OpenAPI response entry, and the `ApiError` type to generate the response schema. This explicit documentation of error cases — rather than relying on runtime error-handling middleware — produces more accurate specs and better developer documentation for API consumers.

## Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on http-errors v2.x, @hapi/boom v10.x, and http-status-codes v2.x.

*[Compare HTTP error handling and API tooling on PkgPulse →](https://www.pkgpulse.com)*

*See also: [h3 vs polka vs koa 2026](/guides/h3-vs-polka-vs-koa-lightweight-http-frameworks-nodejs-2026) and [proxy-agent vs global-agent vs hpagent](/guides/proxy-agent-vs-global-agent-vs-hpagent-http-proxy-2026), [better-sqlite3 vs libsql vs sql.js](/guides/better-sqlite3-vs-libsql-vs-sql-js-sqlite-nodejs-2026).*
