# How to Standardize Error Handling with Cursor

- Tool: Cursor
- Difficulty: Beginner
- Time required: 10-15 min
- Compatibility: Cursor Free+, Express.js/Node.js projects
- Last updated: March 2026

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

## Before you start

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

```
---
description: Standard error handling for Express routes
globs: "*.route.ts,*.controller.ts,*.middleware.ts"
alwaysApply: true
---

# Error Handling Rules
- NEVER send raw Error objects or stack traces to clients
- NEVER use res.status(500).json({ error: err.message }) inline
- ALWAYS throw custom AppError classes from route handlers
- ALWAYS let the global error middleware format the response
- ALWAYS use asyncHandler wrapper for async route handlers

## Standard Error Response Shape:
```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Human-readable error message",
    "details": []
  }
}
```

## Custom Error Classes:
- NotFoundError (404)
- ValidationError (400)
- UnauthorizedError (401)
- ForbiddenError (403)
- ConflictError (409)

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

```
export class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public details?: unknown[]
  ) {
    super(message);
    this.name = 'AppError';
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id?: string) {
    super(404, 'NOT_FOUND', id ? `${resource} with id ${id} not found` : `${resource} not found`);
  }
}

export class ValidationError extends AppError {
  constructor(details: unknown[]) {
    super(400, 'VALIDATION_ERROR', 'Request validation failed', details);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super(401, 'UNAUTHORIZED', message);
  }
}

export class ForbiddenError extends AppError {
  constructor(message = 'Insufficient permissions') {
    super(403, 'FORBIDDEN', message);
  }
}

export class ConflictError extends AppError {
  constructor(message: string) {
    super(409, 'CONFLICT', message);
  }
}
```

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

```
import { Request, Response, NextFunction } from 'express';
import { AppError } from '@/lib/errors';
import logger from '@/lib/logger';

export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  _next: NextFunction
): void => {
  if (err instanceof AppError) {
    res.status(err.statusCode).json({
      success: false,
      error: {
        code: err.code,
        message: err.message,
        details: err.details || [],
      },
    });
    return;
  }

  logger.error({ err, path: req.path, method: req.method }, 'Unhandled error');

  res.status(500).json({
    success: false,
    error: {
      code: 'INTERNAL_ERROR',
      message: 'An unexpected error occurred',
      details: [],
    },
  });
};
```

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

```
@error-handling.mdc @src/lib/errors.ts @src/middleware/error-handler.ts

Create a products router with these endpoints:
- GET /api/products — list with pagination
- GET /api/products/:id — single product (throw NotFoundError if missing)
- POST /api/products — create (throw ValidationError for bad input)
- PUT /api/products/:id — update (NotFoundError or ValidationError)
- DELETE /api/products/:id — delete (NotFoundError if missing)

Use asyncHandler for every route.
Throw custom error classes from @/lib/errors.
Do NOT format error responses in route handlers.
Let 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.

```
@error-handling.mdc @src/routes/products.route.ts

Generate integration tests for the products API error responses:
1. GET /api/products/nonexistent-id returns 404 with standard error shape
2. POST /api/products with invalid body returns 400 with validation details
3. PUT /api/products/:id with non-existent id returns 404
4. DELETE /api/products/:id with non-existent id returns 404
5. Any 500 error returns generic message, never stack trace

Every error response must match this shape:
{ success: false, error: { code: string, message: string, details: [] } }

Use supertest for HTTP assertions.
```

**Expected result:** Cursor generates tests verifying all error responses follow the standard format with no stack trace leakage.

## Complete code example

File: `src/lib/errors.ts`

```typescript
export class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string,
    public details?: unknown[]
  ) {
    super(message);
    this.name = 'AppError';
    Error.captureStackTrace(this, this.constructor);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id?: string) {
    super(404, 'NOT_FOUND', id
      ? `${resource} with id ${id} not found`
      : `${resource} not found`
    );
  }
}

export class ValidationError extends AppError {
  constructor(details: unknown[]) {
    super(400, 'VALIDATION_ERROR', 'Request validation failed', details);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Authentication required') {
    super(401, 'UNAUTHORIZED', message);
  }
}

export class ForbiddenError extends AppError {
  constructor(message = 'Insufficient permissions') {
    super(403, 'FORBIDDEN', message);
  }
}

export class ConflictError extends AppError {
  constructor(message: string) {
    super(409, 'CONFLICT', message);
  }
}

export class RateLimitError extends AppError {
  constructor() {
    super(429, 'RATE_LIMIT', 'Too many requests, please try again later');
  }
}
```

## Common mistakes

- **Cursor formats error responses inline in each route handler** — Without error middleware and custom error classes, Cursor generates try/catch blocks with res.status().json() in every route, creating inconsistent error shapes. Fix: Create custom error classes and a global error middleware. Add a rule that says NEVER format error responses in route handlers.
- **Stack traces leaked in production error responses** — Cursor often includes err.stack or err.message from internal errors in the JSON response, exposing implementation details to clients. Fix: The global error middleware should return a generic message for non-AppError exceptions. Only AppError instances get their message passed through.
- **Different error shapes across routes** — Each Cursor session generates errors independently. One route returns {error: string}, another {message: string}, and another {errors: []}. Fix: 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

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

---

Source: https://www.rapidevelopers.com/cursor-tutorial/how-to-configure-cursor-ai-to-produce-standard-error-objects-in-express-js-routes
© RapidDev — https://www.rapidevelopers.com/cursor-tutorial/how-to-configure-cursor-ai-to-produce-standard-error-objects-in-express-js-routes
