From Business Error to Production Diagnosis
A practical guide to safe frontend errors, protected backend diagnostics, correlation IDs, structured logging, exception handling, and useful observability.
- Error handling
- Observability
- Spring Boot
Full-Stack Developer specializing in Spring Boot
An error response serves two audiences with different needs. The person using the product needs a safe explanation and a next action. The engineer diagnosing production needs precise context, causality, and a way to connect the report to system evidence.
Sending the same detail to both audiences usually fails. Generic messages make support and engineering blind. Raw exception messages expose implementation, provider, or data details to the frontend. A useful error architecture separates the public contract from protected diagnostics while connecting them through a correlation identifier.
A business error means the system understood the request but cannot perform it under current rules. Examples include an invalid state transition, missing prerequisite, unavailable action, duplicate reference, or exceeded policy limit. Designing Reliable Booking and Duty State Transitions shows how those rejections emerge from explicit workflow rules.
An internal error means the system could not complete a valid operation because an implementation or dependency failed. Examples include a database timeout, unexpected null, serialization problem, or unavailable external provider.
The HTTP status alone does not capture this distinction. Both categories need stable application error codes:
| Category | Example code | Public message | Typical handling |
|---|---|---|---|
| Validation | REQUEST_INVALID | Correct the highlighted fields | Show field guidance |
| Workflow | ACTION_NOT_AVAILABLE | Refresh and review the current status | Reload resource state |
| Authority | ACTION_FORBIDDEN | You do not have permission for this action | Stop and preserve context |
| Conflict | RESOURCE_CHANGED | The record changed; refresh before retrying | Refetch current version |
| Dependency | SERVICE_TEMPORARILY_UNAVAILABLE | Try again later or contact support with the reference | Apply controlled retry policy |
| Internal | UNEXPECTED_ERROR | The request could not be completed | Show reference; do not expose cause |
Stable codes let the frontend respond consistently while messages remain editable and localizable.
The frontend contract should contain what the user can act on. A useful generic shape is:
{
"code": "ACTION_NOT_AVAILABLE",
"message": "This action is no longer available. Refresh the record and review its current status.",
"correlationId": "01-example-reference",
"fieldErrors": []
}Avoid messages such as “Something went wrong” when the system knows a safe business reason. Also avoid returning exception class names, SQL fragments, stack traces, hostnames, storage keys, provider payloads, or access-control internals.
The user message should answer one of three questions:
- What can I correct?
- What changed and should I refresh?
- What reference should I provide when I need help?
Protected diagnostics should capture enough context to reconstruct the operation without copying sensitive request bodies into logs.
Useful fields include:
- correlation and trace identifiers;
- application error code;
- operation or use-case name;
- safe resource reference;
- organisation boundary represented by a non-sensitive identifier;
- current workflow state and requested event;
- dependency category and attempt number;
- exception type and controlled stack trace;
- deployment or application version.
The diagnostic event should state what the system was trying to do. A stack trace explains where code failed. It rarely explains the business operation or why that failure mattered.
One failure, two contracts
Public feedback and protected diagnostics share a correlation reference.
Classify failure
Map the exception or business rejection to a stable application category.
Return safe response
Give the user a clear action, stable code, and correlation reference.
Record diagnostics
Store protected context, causality, trace data, and operational impact.
Text alternative
When an operation fails, the backend classifies the failure. It returns a safe error code, message, and correlation identifier to the frontend. Separately, it records protected structured diagnostics using the same correlation identifier so support and engineering can locate the technical evidence.
A correlation ID connects the browser report, API request, background work, integration call, and log events for one operation. It should be generated or accepted at a trusted edge, validated, and propagated through internal calls.
The identifier is not a secret, but it should not encode user data, database IDs, or environment details. Random or time-sortable opaque values are safer.
Return it in the response body or header and include it in every structured diagnostic event for the request. If background work continues after the response, keep the original correlation ID and add a separate job or attempt ID rather than replacing context.
Structured logging treats a log event as named fields rather than a formatted sentence that must be parsed later.
log.atError()
.addKeyValue("errorCode", "EXTERNAL_OPERATION_FAILED")
.addKeyValue("operation", "generateDocument")
.addKeyValue("correlationId", context.correlationId())
.addKeyValue("attempt", attempt.number())
.setCause(exception)
.log("Deferred operation failed");Choose fields that support diagnosis and aggregation. Avoid logging entire domain objects “just in case.” That creates noisy, expensive, and potentially sensitive telemetry.
Logging levels should also communicate intent. A rejected business transition may be an informational or warning event, not an application error. A provider timeout that will be retried may be a warning on early attempts and an error only when the retry policy is exhausted.
Central exception handling creates one public error contract, but it should not flatten every failure into the same status and message.
@RestControllerAdvice
final class ApiExceptionHandler {
@ExceptionHandler(InvalidTransition.class)
ResponseEntity<ApiError> invalidTransition(
InvalidTransition exception,
RequestContext context
) {
diagnostics.record(exception, context);
return ResponseEntity.status(409).body(new ApiError(
"ACTION_NOT_AVAILABLE",
"This action is no longer available. Refresh and try again.",
context.correlationId()
));
}
}Keep mapping focused. The handler chooses the public contract and records or delegates diagnostics. Business rules should not live in the exception handler, and provider-specific exceptions should normally be translated at the integration boundary before reaching it.
Logs are one part of observability. A production diagnosis may also need metrics and traces:
- Metrics show rate, volume, latency, and saturation. They answer whether a problem is isolated or systemic.
- Traces show the path and timing across application, database, job, and external boundaries.
- Logs provide discrete diagnostic events and controlled exception detail.
- Business signals show operational impact: queued jobs, failed documents, blocked workflows, or records awaiting intervention.
Alerts should describe actionable conditions. “Five exceptions occurred” is less useful than “document generation has exhausted retries for a sustained period and work is accumulating.” The second connects technical symptoms to operational impact.
Error handling should assume that messages, headers, and response bodies can be observed outside the engineering team. Do not expose:
- credentials, tokens, or signed URLs;
- database queries or schema names;
- internal hostnames and network topology;
- file paths or storage keys;
- provider account details or raw provider responses;
- customer, passenger, driver, or employee data;
- authorization rules that make probing easier;
- stack traces or framework versions.
Redaction should happen before data reaches the logging sink, not only in a dashboard. Access to diagnostic systems should follow role and retention policies. Test the public response separately from the diagnostic event so improving one does not accidentally weaken the other.
The design goal is a productive asymmetry: the frontend receives less technical detail but more useful guidance; engineering receives more technical context, but only inside protected, searchable, and accountable systems.
Start with stable application error codes, propagate one correlation reference, and record structured evidence around the business operation—not only the exception. That combination makes incidents easier to diagnose without turning the public API into a window onto production internals.