Help

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, expectedreceived 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 handler

Decorator 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.
Was this helpful?

On this page