Skip to main content

http-errors vs @hapi/boom vs http-status-codes: HTTP Error Handling in Node.js (2026)

·PkgPulse Team

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 — standard HTTP error factory:

Basic usage

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

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

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

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 — rich HTTP error objects:

Basic usage

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

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

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

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

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 — status code constants:

Status code enums

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

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

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

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

Featurehttp-errors@hapi/boomhttp-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 byExpress, KoaHapiAny 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

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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.