Practical error contracts for PHP APIs

Designing stable HTTP status, error code, field validation, correlation, and retry semantics for frontend and integration teams.

PHPREST APIError HandlingBackend Development
Structured API response cards showing success and typed PHP errors

An API error becomes useful when a client can decide what to do next without parsing an English sentence. The response should separate a stable machine code from a readable message and include field-level details only when they help the caller correct input.

Define the contract before the controller

Choose a small envelope with a code, message, correlation ID, and optional validation errors. Document when a request can be retried and whether an idempotency key is supported. Avoid returning stack traces, SQL messages, file paths, or third-party credentials.

An envelope that separates audiences

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Review the highlighted fields.",
    "correlation_id": "req_01J...",
    "fields": {
      "email": ["Enter a valid email address."]
    }
  }
}

The stable code is for program logic. The message is for a person and can be translated or improved. The correlation ID connects the response to protected server telemetry. Field errors are appropriate for correctable input but should not reveal whether a private account, email address or token exists.

Use HTTP status deliberately

Distinguish malformed input, failed authentication, missing authorization, absent resources, conflicts, rate limits, and server failures. A consistent status-code policy makes browser, mobile, and partner integrations easier to test.

  • 400 means the request itself cannot be understood or violates a basic contract.
  • 401 means valid authentication is missing or no longer accepted.
  • 403 means the known identity is not allowed to perform the action.
  • 404 means the exposed resource cannot be found; it can also avoid leaking private-resource existence.
  • 409 means the request conflicts with current state, such as an optimistic-lock or uniqueness conflict.
  • 422 can represent well-formed input that fails domain validation when that convention is documented.
  • 429 means the caller is rate limited and should include useful retry headers.
  • 500 represents an unexpected server failure; 502, 503 and 504 can describe dependency or availability boundaries.

The exact mapping matters less than consistency. Do not return 200 with an error body for convenience; intermediaries, monitoring and client libraries use the status line.

Map domain failures in one place

Controllers become inconsistent when each one catches exceptions and invents a response. Define application exceptions or result types for expected conditions, then map them at the HTTP boundary. Unexpected exceptions should pass to a final handler that records the full internal context and returns a generic stable response.

In PHP, set Content-Type: application/json; charset=utf-8, choose the status before output, and encode with JSON_THROW_ON_ERROR. A failure to encode the error response is still a server failure, so scalar, bounded context is safer than placing arbitrary exception objects into the envelope.

Validation errors need predictable paths

Nested payloads require a documented field-path convention such as items.0.quantity. Keep error values as arrays so more than one rule can be reported without changing the type later. Return only checks the client can act on; authorization and internal integrity failures do not belong beside form labels.

Make failures observable

Return a correlation ID and write the same identifier to structured server logs. Log enough context to diagnose the path while redacting tokens and private data. For dependency failures, record the upstream status and latency privately while exposing a stable application-level error.

Propagate, but do not trust, request IDs

A gateway may provide a request ID, but arbitrary client values should be validated and length-limited before entering logs. Generate a server ID when necessary and propagate it to trusted downstream services. Include route name, authenticated subject identifier, duration and outcome, while excluding passwords, cookies, authorization headers and raw payment details.

Tell clients when a retry is safe

A retry contract needs more than a 5xx status. State whether the operation is idempotent, support an idempotency key for create or payment operations, and send Retry-After for known rate or maintenance windows. Clients should use bounded exponential backoff with jitter and should not retry validation, authentication or permission failures automatically.

Timeouts are ambiguous: the server may have committed the operation even though the client did not receive the response. An idempotency record lets the client repeat the same key and obtain the original result instead of creating a duplicate.

Version the meaning carefully

Adding an optional field is usually backward-compatible. Renaming a code, changing a field from string to array, or moving a status to a different semantic category can break consumers. Publish a code catalog, mark deprecated codes, and collect unknown-code telemetry in first-party clients before removing old behavior.

Test behavior rather than wording

Contract tests should assert status, code, response shape, relevant headers, and retry semantics. Human-readable wording can improve without forcing every API consumer to release a matching change.

Production-ready checklist

  1. Trigger every documented expected failure through an HTTP integration test.
  2. Verify unauthenticated and unauthorized responses do not disclose private resource existence.
  3. Force database, cache and upstream timeouts and confirm a bounded generic response.
  4. Check every response has a valid correlation ID and every matching log entry is redacted.
  5. Send duplicate idempotency keys concurrently and prove only one operation is committed.
  6. Validate response examples against the same schema used by documentation or client generation.

A good error contract reduces coordination cost. Frontend code can choose the correct state, partners can integrate without sentence parsing, and operators can move from a public correlation ID to the private cause without exposing that cause to the caller.

Review the contract with the people who consume it before freezing names. A backend-friendly code may still be too broad for a mobile recovery flow, while an extremely specific code list can expose implementation details and become impossible to maintain. Start with durable business meanings, collect real unknown cases, and expand deliberately.

Discuss a project