Best Practices

AI Rules for Error Codes and Messages

AI returns 'Something went wrong' for every error. Rules for structured error responses, machine-readable codes, user-facing messages, field-level errors, and error documentation links.

7 min read·January 28, 2025

'Something went wrong' — useless to users, useless to developers, useless to client code

Structured errors, machine-readable codes, field-level details, docs links in every error

AI Returns the Same Error for Everything

AI generates error responses with: generic messages ('Something went wrong', 'Bad request', 'Internal server error'), no error codes (clients cannot programmatically handle errors), no field-level details (which field failed? what was expected?), inconsistent formats (string error in one endpoint, object in another), and no actionable guidance (what should the user or developer do?). Clients cannot distinguish a validation error from a server crash.

Modern error responses are: structured (consistent JSON format across all endpoints), coded (machine-readable error code like VALIDATION_ERROR for client logic), messaged (human-readable description for developers and users), detailed (field-level errors for forms, constraint details for business rules), and linked (docs URL pointing to the full error explanation and fix). AI generates none of these.

These rules cover: consistent error response structure, error code taxonomy, user-facing vs developer-facing messages, field-level validation details, and documentation links in error payloads.

Rule 1: Consistent Error Response Structure

The rule: 'Every error response follows the same structure: { "error": { "code": "VALIDATION_ERROR", "message": "Email is not valid", "details": { "field": "email", "value": "notanemail", "constraint": "must be valid email format" }, "requestId": "req_abc123", "docs": "https://docs.example.com/errors/validation-error" } }. The structure is identical for 400s, 401s, 403s, 404s, and 500s — only the content changes. Clients parse one format, handle all errors.'

For the error envelope: 'Always wrap errors in an { error: {} } envelope — never return a bare string or array. The envelope: identifies the response as an error (check for error key), is extensible (add fields without breaking clients), and is distinguishable from success responses (success has data key, error has error key). Pattern: { data } for success, { error } for failure — never mix both.'

AI generates: res.json({ message: 'error' }) in one endpoint, res.json('error') in another, res.json({ errors: ['error'] }) in a third. Clients need three different parsers. One consistent structure means one error handler for the entire API — write it once, use it everywhere.

  • Consistent structure: { error: { code, message, details, requestId, docs } }
  • Same format for all error status codes — 400, 401, 403, 404, 500
  • Error envelope: always { error: {} }, never bare string or array
  • Success: { data }, Error: { error } — never mix both in one response
  • requestId in every error — enables cross-referencing with server logs
💡 One Parser, All Errors

AI generates three different error formats across three endpoints. Clients need three parsers. One consistent structure — { error: { code, message, details, requestId, docs } } — means one error handler for the entire API. Write it once, never revisit.

Rule 2: Machine-Readable Error Code Taxonomy

The rule: 'Define a finite set of error codes that clients can switch on: VALIDATION_ERROR, AUTHENTICATION_REQUIRED, PERMISSION_DENIED, RESOURCE_NOT_FOUND, RATE_LIMITED, CONFLICT, INTERNAL_ERROR. Error codes are: stable (never change once published), documented (each code has a docs page), and actionable (the code tells the client what to do: retry, re-authenticate, fix input, contact support).'

For code naming: 'Use SCREAMING_SNAKE_CASE for error codes — consistent, unambiguous, conventional. Prefix with domain for large APIs: USER_NOT_FOUND, ORDER_ALREADY_SHIPPED, PAYMENT_DECLINED. The code is the machine contract — clients build switch statements on it. The message is the human contract — it can change, improve, or be translated without breaking clients.'

AI generates: no error codes. The client parses the message string: if (error.message.includes('not found')). When you improve the message from 'not found' to 'User not found — check the ID format', the client parsing breaks. Error codes are stable identifiers; messages are mutable descriptions. Clients use codes; humans read messages.

⚠️ Parsing Messages = Fragile

if (error.message.includes('not found')) — client logic based on message strings. Improve the message from 'not found' to 'User not found' and the parsing breaks. Error codes are the stable contract; messages are the mutable description.

Rule 3: User-Facing vs Developer-Facing Messages

The rule: 'Provide two message levels: message (safe to show to end users — clear, non-technical, actionable) and developerMessage (detailed, technical, for debugging). User message: "This email is already registered. Try signing in instead." Developer message: "Unique constraint violation on users.email: 'jane@example.com' already exists (constraint: users_email_unique)." The user message drives UX; the developer message drives debugging.'

For message writing: 'User messages should: explain what happened (not how), suggest what to do next, avoid technical jargon (no 'constraint violation', 'null pointer', or 'timeout'), and never expose internal details (no table names, SQL, or stack traces). Developer messages should: include the technical cause, reference the constraint or system, include relevant values (the email that conflicted), and help reproduce the issue.'

AI generates: 'Internal Server Error' — useless to both users and developers. The user does not know what happened or what to do. The developer does not know which system failed or why. Two messages — one for each audience — make errors actionable for everyone.

Rule 4: Field-Level Validation Error Details

The rule: 'For validation errors (400), include field-level details: { "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "details": { "fields": { "email": { "message": "Must be a valid email", "value": "notanemail", "constraint": "email" }, "password": { "message": "Must be at least 8 characters", "value": null, "constraint": "minLength:8" } } } } }. All field errors at once — never one at a time. Clients map these to form fields for inline display.'

For the details object: 'The details object is type-specific: validation errors have fields, rate limiting has retryAfter and limit, conflict errors have existingResourceId and conflictingField. The details schema is documented per error code — clients know exactly what to expect for VALIDATION_ERROR vs RATE_LIMITED vs CONFLICT. This is the contract: code tells you the error type, details tells you the specifics.'

AI generates: { error: 'Invalid email' } — one field, one error, no structure. The form shows a generic error, the user fixes email, submits, discovers the password is too short too. Field-level details: all errors at once, mapped to inputs, fixed in one pass.

Complete Error Codes and Messages Rules Template

Consolidated rules for error codes and messages.

  • Consistent structure: { error: { code, message, details, requestId, docs } }
  • Machine-readable codes: VALIDATION_ERROR, RATE_LIMITED — stable, documented, actionable
  • User message (non-technical, actionable) + developer message (technical, debuggable)
  • Field-level validation details: all fields, all errors, mapped to form inputs
  • Details object varies by error code — documented schema per code
  • Docs URL in every error — links to cause, fix, code examples, related errors
  • requestId for log correlation — trace the error through your system
  • Never expose internal details to users — no SQL, no stack traces, no table names