Skip to main content
RapidDev - Software Development Agency
cursor-tutorial

How to Standardize Error Handling with Cursor

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.

What you'll learn

  • How to define a standard error response format for Cursor to follow
  • How to create error handling middleware that Cursor uses automatically
  • How to prevent Cursor from leaking stack traces in error responses
  • How to generate custom error classes for different HTTP status codes
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read10-15 minCursor Free+, Express.js/Node.js projectsMarch 2026RapidDev Engineering Team
TL;DR

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

1

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.

.cursor/rules/error-handling.mdc
1---
2description: Standard error handling for Express routes
3globs: "*.route.ts,*.controller.ts,*.middleware.ts"
4alwaysApply: true
5---
6
7# Error Handling Rules
8- NEVER send raw Error objects or stack traces to clients
9- NEVER use res.status(500).json({ error: err.message }) inline
10- ALWAYS throw custom AppError classes from route handlers
11- ALWAYS let the global error middleware format the response
12- ALWAYS use asyncHandler wrapper for async route handlers
13
14## Standard Error Response Shape:
15```json
16{
17 "success": false,
18 "error": {
19 "code": "VALIDATION_ERROR",
20 "message": "Human-readable error message",
21 "details": []
22 }
23}
24```
25
26## Custom Error Classes:
27- NotFoundError (404)
28- ValidationError (400)
29- UnauthorizedError (401)
30- ForbiddenError (403)
31- ConflictError (409)
32
33Import from @/lib/errors

Expected result: Cursor generates routes that throw custom error classes instead of formatting error responses inline.

2

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.

src/lib/errors.ts
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}
12
13export 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}
18
19export class ValidationError extends AppError {
20 constructor(details: unknown[]) {
21 super(400, 'VALIDATION_ERROR', 'Request validation failed', details);
22 }
23}
24
25export class UnauthorizedError extends AppError {
26 constructor(message = 'Authentication required') {
27 super(401, 'UNAUTHORIZED', message);
28 }
29}
30
31export class ForbiddenError extends AppError {
32 constructor(message = 'Insufficient permissions') {
33 super(403, 'FORBIDDEN', message);
34 }
35}
36
37export 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.

3

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.

src/middleware/error-handler.ts
1import { Request, Response, NextFunction } from 'express';
2import { AppError } from '@/lib/errors';
3import logger from '@/lib/logger';
4
5export const errorHandler = (
6 err: Error,
7 req: Request,
8 res: Response,
9 _next: NextFunction
10): 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 }
22
23 logger.error({ err, path: req.path, method: req.method }, 'Unhandled error');
24
25 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.

4

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.

Cmd+L prompt
1@error-handling.mdc @src/lib/errors.ts @src/middleware/error-handler.ts
2
3Create a products router with these endpoints:
4- GET /api/products list with pagination
5- 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)
9
10Use 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.

5

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.

Cmd+L prompt
1@error-handling.mdc @src/routes/products.route.ts
2
3Generate integration tests for the products API error responses:
41. GET /api/products/nonexistent-id returns 404 with standard error shape
52. POST /api/products with invalid body returns 400 with validation details
63. PUT /api/products/:id with non-existent id returns 404
74. DELETE /api/products/:id with non-existent id returns 404
85. Any 500 error returns generic message, never stack trace
9
10Every error response must match this shape:
11{ success: false, error: { code: string, message: string, details: [] } }
12
13Use 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

src/lib/errors.ts
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}
13
14export class NotFoundError extends AppError {
15 constructor(resource: string, id?: string) {
16 super(404, 'NOT_FOUND', id
17 ? `${resource} with id ${id} not found`
18 : `${resource} not found`
19 );
20 }
21}
22
23export class ValidationError extends AppError {
24 constructor(details: unknown[]) {
25 super(400, 'VALIDATION_ERROR', 'Request validation failed', details);
26 }
27}
28
29export class UnauthorizedError extends AppError {
30 constructor(message = 'Authentication required') {
31 super(401, 'UNAUTHORIZED', message);
32 }
33}
34
35export class ForbiddenError extends AppError {
36 constructor(message = 'Insufficient permissions') {
37 super(403, 'FORBIDDEN', message);
38 }
39}
40
41export class ConflictError extends AppError {
42 constructor(message: string) {
43 super(409, 'CONFLICT', message);
44 }
45}
46
47export 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.

ChatGPT Prompt

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.

Cursor Prompt

@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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.