Skip to content

[fix] ctx.assert() throws specific error classes #1925

Closed

Description

@isjs-d

Description: The JSDoc for HttpError in Koa states:

A re-export of HttpError from the http-error package. This is the error type that is thrown by ctx.assert() and ctx.throw().

However, ctx.assert() actually throws specific error class, not the HttpError class. This breaks instanceof HttpError checks in error handling.

Code to reproduce

import { HttpError } from 'koa'

const router = new Router()

router.get('/test', async (ctx) => {
  try {
    const data = null
    ctx.assert(data, 401, 'Unauthorized')
  } catch (e) {
    console.log(e instanceof HttpError) // ❌ false - should be true
  }
})

Activity

  1. isjs-d commented on Jan 25, 2026

    @isjs-d
    Author

    Update: This is likely an issue with http-assert, which uses: "http-errors": "~1.8.0"
    while Koa uses: "http-errors": "^2.0.0"

  2. Jerry-CodeHub commented on Jan 29, 2026

    @Jerry-CodeHub
    Contributor

    Reason
    This issue involves the problem of cross-module instance checks with instanceof in JavaScript and the complexity of Koa's dependency chain.

    Dependency chain:
    Koa
    ├── ctx.throw() → Use http-errors directly
    ├── ctx.assert() → Use http-assert → Indirectly use http-errors
    └── Export HttpError → From http-errors

    Question:

    1. http-errors creates specific error subclasses: When calling createError(401, 'Unauthorized'), what is actually returned is an instance of the UnauthorizedError class, not the HttpError class itself.
    2. Multiple instance issue: If http-assert and koa each depend on different versions of http-errors, then their HttpError base classes are actually different constructor instances, causing instanceof checks to fail.
    3. npm dependency resolution: In npm/yarn/pnpm, if the version ranges are different, multiple copies of http-errors may be installed:

    node_modules/
    ├── http-errors/ # The version of koa's dependency
    └── http-assert/
    └── node_modules/
    └── http-errors/ # Different versions of http-assert dependencies

    Verification:

    import { HttpError } from 'koa'
    import createError from 'http-errors'
    
    // ctx.throw() The behavior of...
    const throwError = createError(401, 'Unauthorized')
    console.log(throwError instanceof HttpError)        // ✅ true (from the same http-errors)
    console.log(throwError instanceof createError.HttpError) // ✅ true
    console.log(throwError.constructor.name)            // 'UnauthorizedError'
    
    // The behavior of ctx.assert() - different versions of http-errors may be used
    // If the versions are inconsistent, the instanceof check will fail

    Solution:

    Use createError.isHttpError() for checking (recommended)

    The http-errors module provides the isHttpError() function, which uses duck typing and does not rely on instanceof:

    import createError from 'http-errors'
    
    app.use(async (ctx, next) => {
      try {
        await next()
      } catch (err) {
        // ✅ Recommendation: Use isHttpError for the check.
        if (createError.isHttpError(err)) {
          ctx.status = err.status
          ctx.body = { message: err.message }
        } else {
          ctx.status = 500
          ctx.body = { message: 'Internal Server Error' }
          ctx.app.emit('error', err, ctx)
        }
      }
    })

    The internal implementation of isHttpError() is based on duck typing:

    // The implementation logic of the isHttpError method in http-errors
    function isHttpError(val) {
      if (!val || typeof val !== 'object') return false
      
      // Check if it is an instance of HttpError
      if (val instanceof HttpError) return true
      
      // Duck type checking
      return typeof val.status === 'number' &&
             typeof val.statusCode === 'number' &&
             typeof val.expose === 'boolean'
    }
  3. added a commit that references this issue on Mar 7, 2026
    943b7d2
  4. added a commit that references this issue on Mar 18, 2026
    6f653d2
  5. added a commit that references this issue on Sep 2, 2026
    1061776
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions