Cursor generates inconsistent error responses across Express.js routes, with some returning plain strings, others returning objects with different shapes, and some leaking stack traces to clients. By creating .cursor/rules/ with a standard error format, building a shared error handling middleware, and prompting Cursor with your error contract, you get consistent, secure error responses across every endpoint.
Standardizing error handling with Cursor
Inconsistent error responses make APIs difficult for clients to consume. When Cursor generates each route independently, each one handles errors differently. This tutorial establishes a standard error format, creates reusable error utilities, and configures Cursor to produce consistent error handling across all Express.js routes.
Prerequisites
- Cursor installed with an Express.js/TypeScript project
- Basic understanding of Express middleware
- Familiarity with HTTP status codes
- Understanding of Cursor project rules
Step-by-step guide
Create an error handling rule for Cursor
Create an error handling rule for Cursor
Add a project rule that defines your standard error response format and tells Cursor to use custom error classes instead of ad-hoc error handling in each route.
1---2description: Standard error handling for Express routes3globs: "*.route.ts,*.controller.ts,*.middleware.ts"4alwaysApply: true5---67# Error Handling Rules8- NEVER send raw Error objects or stack traces to clients9- NEVER use res.status(500).json({ error: err.message }) inline10- ALWAYS throw custom AppError classes from route handlers11- ALWAYS let the global error middleware format the response12- ALWAYS use asyncHandler wrapper for async route handlers1314## Standard Error Response Shape:15```json16{17 "success": false,18 "error": {19 "code": "VALIDATION_ERROR",20 "message": "Human-readable error message",21 "details": []22 }23}24```2526## Custom Error Classes:27- NotFoundError (404)28- ValidationError (400)29- UnauthorizedError (401)30- ForbiddenError (403)31- ConflictError (409)3233Import from @/lib/errorsExpected result: Cursor generates routes that throw custom error classes instead of formatting error responses inline.
Create custom error classes and middleware
Create custom error classes and middleware
Build the error infrastructure that Cursor imports. Custom error classes carry status codes and error codes, while the global middleware formats them into the standard response shape.
1export class AppError extends Error {2 constructor(3 public statusCode: number,4 public code: string,5 message: string,6 public details?: unknown[]7 ) {8 super(message);9 this.name = 'AppError';10 }11}1213export class NotFoundError extends AppError {14 constructor(resource: string, id?: string) {15 super(404, 'NOT_FOUND', id ? `${resource} with id ${id} not found` : `${resource} not found`);16 }17}1819export class ValidationError extends AppError {20 constructor(details: unknown[]) {21 super(400, 'VALIDATION_ERROR', 'Request validation failed', details);22 }23}2425export class UnauthorizedError extends AppError {26 constructor(message = 'Authentication required') {27 super(401, 'UNAUTHORIZED', message);28 }29}3031export class ForbiddenError extends AppError {32 constructor(message = 'Insufficient permissions') {33 super(403, 'FORBIDDEN', message);34 }35}3637export class ConflictError extends AppError {38 constructor(message: string) {39 super(409, 'CONFLICT', message);40 }41}Expected result: Custom error classes that Cursor imports when generating route handlers.
Create the global error handling middleware
Create the global error handling middleware
Build a centralized error handler that formats all errors into the standard response shape. This middleware catches errors thrown by route handlers and returns a consistent JSON response without leaking implementation details.
1import { Request, Response, NextFunction } from 'express';2import { AppError } from '@/lib/errors';3import logger from '@/lib/logger';45export const errorHandler = (6 err: Error,7 req: Request,8 res: Response,9 _next: NextFunction10): void => {11 if (err instanceof AppError) {12 res.status(err.statusCode).json({13 success: false,14 error: {15 code: err.code,16 message: err.message,17 details: err.details || [],18 },19 });20 return;21 }2223 logger.error({ err, path: req.path, method: req.method }, 'Unhandled error');2425 res.status(500).json({26 success: false,27 error: {28 code: 'INTERNAL_ERROR',29 message: 'An unexpected error occurred',30 details: [],31 },32 });33};Expected result: A global error middleware that formats all errors consistently and hides internal details.
Generate routes that use the error pattern
Generate routes that use the error pattern
Prompt Cursor to generate new routes referencing both the error rule and the error classes. Cursor will throw custom errors instead of formatting responses inline, keeping route handlers clean and consistent.
1@error-handling.mdc @src/lib/errors.ts @src/middleware/error-handler.ts23Create a products router with these endpoints:4- GET /api/products — list with pagination5- GET /api/products/:id — single product (throw NotFoundError if missing)6- POST /api/products — create (throw ValidationError for bad input)7- PUT /api/products/:id — update (NotFoundError or ValidationError)8- DELETE /api/products/:id — delete (NotFoundError if missing)910Use asyncHandler for every route.11Throw custom error classes from @/lib/errors.12Do NOT format error responses in route handlers.13Let the global error middleware handle all error formatting.Expected result: Cursor generates clean route handlers that throw custom errors, with no inline error response formatting.
Test error responses for consistency
Test error responses for consistency
Use Cursor to generate tests that verify all endpoints return errors in the standard format. This ensures the error middleware works correctly and no route bypasses it with inline error handling.
1@error-handling.mdc @src/routes/products.route.ts23Generate integration tests for the products API error responses:41. GET /api/products/nonexistent-id returns 404 with standard error shape52. POST /api/products with invalid body returns 400 with validation details63. PUT /api/products/:id with non-existent id returns 40474. DELETE /api/products/:id with non-existent id returns 40485. Any 500 error returns generic message, never stack trace910Every error response must match this shape:11{ success: false, error: { code: string, message: string, details: [] } }1213Use supertest for HTTP assertions.Expected result: Cursor generates tests verifying all error responses follow the standard format with no stack trace leakage.
Complete working example
1export class AppError extends Error {2 constructor(3 public statusCode: number,4 public code: string,5 message: string,6 public details?: unknown[]7 ) {8 super(message);9 this.name = 'AppError';10 Error.captureStackTrace(this, this.constructor);11 }12}1314export class NotFoundError extends AppError {15 constructor(resource: string, id?: string) {16 super(404, 'NOT_FOUND', id17 ? `${resource} with id ${id} not found`18 : `${resource} not found`19 );20 }21}2223export class ValidationError extends AppError {24 constructor(details: unknown[]) {25 super(400, 'VALIDATION_ERROR', 'Request validation failed', details);26 }27}2829export class UnauthorizedError extends AppError {30 constructor(message = 'Authentication required') {31 super(401, 'UNAUTHORIZED', message);32 }33}3435export class ForbiddenError extends AppError {36 constructor(message = 'Insufficient permissions') {37 super(403, 'FORBIDDEN', message);38 }39}4041export class ConflictError extends AppError {42 constructor(message: string) {43 super(409, 'CONFLICT', message);44 }45}4647export class RateLimitError extends AppError {48 constructor() {49 super(429, 'RATE_LIMIT', 'Too many requests, please try again later');50 }51}Common mistakes when standardizing Error Handling with Cursor
Why it's a problem: Cursor formats error responses inline in each route handler
How to avoid: Create custom error classes and a global error middleware. Add a rule that says NEVER format error responses in route handlers.
Why it's a problem: Stack traces leaked in production error responses
How to avoid: The global error middleware should return a generic message for non-AppError exceptions. Only AppError instances get their message passed through.
Why it's a problem: Different error shapes across routes
How to avoid: Define a single error response shape in your rules and test for it. The global middleware enforces the shape regardless of what individual routes do.
Best practices
- Define a single error response shape and document it in your API specification
- Use custom error classes with status codes and error codes instead of raw Error objects
- Create a global error middleware that formats all errors consistently
- Never leak stack traces or internal error messages to API clients
- Use asyncHandler to catch async errors and forward them to the error middleware
- Test error responses with the same rigor as success responses
- Log full error details server-side while returning safe summaries to clients
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Create a standardized error handling system for an Express.js TypeScript API with custom error classes (NotFoundError, ValidationError, UnauthorizedError), a global error middleware, and an asyncHandler utility. Show the standard error response JSON shape.
@error-handling.mdc @src/lib/errors.ts @src/middleware/error-handler.ts Generate an orders API with full CRUD endpoints. Every route must use asyncHandler and throw custom errors from @/lib/errors. No inline error response formatting. Let the global error middleware handle all error responses.
Frequently asked questions
Should I use HTTP status codes or custom error codes?
Use both. HTTP status codes for the response status, and custom error codes (like VALIDATION_ERROR) in the response body for programmatic error handling by clients.
How do I handle validation errors from Zod?
Create a middleware that catches Zod errors and converts them to your ValidationError class with the Zod error details in the details array.
Should I use error handling middleware or try/catch in routes?
Use the global error middleware. Route handlers should throw errors and let the middleware catch them. Use asyncHandler to bridge async errors to the Express error chain.
How do I handle errors from third-party services?
Catch third-party errors in your service layer and rethrow them as AppError subclasses with appropriate status codes. Never let raw third-party errors reach the client.
What about GraphQL error handling?
GraphQL has its own error format. Create a similar pattern with custom error classes but format them according to the GraphQL specification with extensions for error codes.
Can RapidDev help standardize our API error handling?
Yes. RapidDev designs API error handling architectures with custom error classes, global middleware, logging integration, and Cursor rules for consistent error patterns across all endpoints.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation