Troubleshooting
Real error messages from NextRush, what causes them, and how to fix them.
Error → cause → fix, for messages NextRush actually produces. Every error class here is
verified against packages/errors/src. If your error isn't listed, check the
FAQ or open an issue.
Error response shape
Every typed error extends NextRushError and serializes through toJSON():
{ error, message, code, status }, plus details and cause (both only when expose is
true — cause is redacted to { name, message, code }, nested one level for a wrapped
cause chain, never the raw object) and, when set, requestId / traceId / timestamp.
ValidationError additionally serializes an issues array. See
packages/errors/src/base.ts.
HTTP errors (@nextrush/errors)
NotFoundError — 404 Not Found
Cause: You threw new NotFoundError(...), or no route matched the request path/method and
nothing else produced a 404.
Fix: Confirm the route is registered on the router you mounted with app.route(prefix, router), and that the HTTP method matches. Throw it explicitly where a lookup fails:
import { NotFoundError } from '@nextrush/errors';
const user = await db.users.findById(ctx.params.id);
if (!user) throw new NotFoundError('User not found');ValidationError — 400 Bad Request, code: "VALIDATION_ERROR"
Cause: Thrown by @nextrush/validation's validate() (or a @Body({ transform })
decorator) when a Standard Schema library (Zod, Valibot, ArkType) rejects the input. The
response body includes an issues array (path, message, rule, expected — received is
always stripped from the response to avoid leaking submitted values such as passwords or tokens).
Fix: Read issues client-side to show field-level messages, or call
error.getFirstError('fieldName') server-side. See the Validation guide.
UnauthorizedError — 401 Unauthorized
Cause: Thrown by your own auth middleware/guard, or createError(401, ...) / unauthorized(...)
from @nextrush/errors' factory functions.
Fix: Confirm authentication middleware runs before the route handler and that it throws
(not merely sets ctx.status) when credentials are missing or invalid — see
Authentication.
TooManyRequestsError — 429 Too Many Requests
Cause: @nextrush/rate-limit (or your own code) throwing when a client exceeds its
configured limit. The error optionally carries a retryAfter field surfaced in details.
Fix: Respect the Retry-After semantics on the client, or raise the configured limit/window
if the limit is too strict for real traffic. See Production: Scaling.
A 5xx error's message field says "Internal Server Error" even though I passed a real message
Cause: This is intentional, not a bug. Every HttpError subclass at 500+ (InternalServerError,
BadGatewayError, ServiceUnavailableError, GatewayTimeoutError, and the rest of the 5xx family
in packages/errors/src/http-errors.ts) is constructed with expose: false by default.
NextRushError.toJSON() only serializes the real message when expose is true — for
non-exposed errors it substitutes the generic status message so internal detail never reaches a
client response body.
Fix: Log the real error server-side (error.message, error.cause, error.stack — never
gated by expose) and, if you deliberately want a specific message exposed to the client, pass
{ expose: true } in the error's options:
import { InternalServerError } from '@nextrush/errors';
throw new InternalServerError('Upstream payment provider unavailable', { expose: true });My custom error's details aren't showing up in the response
Cause: details is only serialized when expose is true (see toJSON() in
packages/errors/src/base.ts). 4xx errors default to expose: true; 5xx errors default to
expose: false.
Fix: For a 5xx error where you intentionally want details visible, pass { expose: true, details: {...} } explicitly. For 4xx errors, details should already appear — confirm you're
reading the field before any custom errorHandler({ transform }) overwrites the body.
My error middleware isn't catching thrown errors
Cause: errorHandler() from @nextrush/errors wraps next() in a try/catch — it only
catches errors thrown downstream of where it's registered. If it's registered after the
route that throws, the error already propagated past it.
Fix: Register errorHandler() early in the middleware chain, before your routes:
import { errorHandler } from '@nextrush/errors';
app.use(errorHandler({ includeStack: process.env.NODE_ENV !== 'production' }));
app.route('/', router); // routes registered after the handlerDecorator metadata / DI setup
reflect-metadata errors, or DI resolves undefined for constructor parameters
Cause: TypeScript's emitDecoratorMetadata compiler option wasn't enabled, or your build
tool strips decorator metadata even when the flag is set. tsx and esbuild-based tool-chains do
not emit emitDecoratorMetadata output — this is a known limitation of those transpilers, not a
NextRush bug.
Fix: For class-based code (nextrush/class, DI), set "experimentalDecorators": true and
"emitDecoratorMetadata": true in tsconfig.json (see
Installation → Configure TypeScript), and for
production builds use nextrush build (from @nextrush/dev), which compiles with SWC
specifically because it preserves this metadata — see the
Dev Tools guide for the full explanation and setup.
nextrush dev works but nextrush build output loses DI behavior
Cause: A dev-only script (tsx, plain ts-node, or a bundler without decorator-metadata
support) is being used for the production build too.
Fix: Use nextrush build for production builds — it's built specifically to solve this by
compiling with SWC. See Dev Tools.
Common setup issues
Cannot find module 'nextrush/class' or similar subpath import errors
Cause: An outdated nextrush version, or a package.json "exports" misconfiguration in a
non-standard bundler that doesn't respect subpath exports.
Fix: Confirm your installed version on the
Compatibility Matrix and update if needed
(pnpm add nextrush@latest). Most modern bundlers (Vite, esbuild, tsx, Node.js ESM) resolve
subpath exports correctly out of the box.
An import compiles but the symbol doesn't exist at runtime
Cause: Importing a symbol from the wrong package — for example, importing DI-only symbols
like @Config, @Injectable, @Optional, or delay from nextrush/class instead of
@nextrush/di. nextrush/class re-exports only Service, Repository, container,
createContainer, inject, and Container from @nextrush/di — everything else in that
package must be imported from @nextrush/di directly.
Fix: Check the DI reference for the exact re-export surface, or
import directly from @nextrush/di for anything not on that list.
Routes return 404 even though the handler is registered
Cause: The router instance was created but never mounted with app.route(prefix, router),
or the prefix doesn't match the request path.
Fix: Confirm createRouter() (from @nextrush/router, or re-exported via nextrush) is
mounted:
import { createApp, createRouter, listen } from 'nextrush';
const app = createApp();
const router = createRouter();
router.get('/users', (ctx) => ctx.json([]));
app.route('/', router); // mounting step — easy to forget
await listen(app, 8080);Still stuck?
- Check the FAQ for design and setup questions that aren't error messages.
- Check the Glossary if a term (Context, Middleware, Extension) is unclear.
- Open an issue with your Node.js version, NextRush version (see the Compatibility Matrix), and a minimal reproduction.