Error Contract - Quick Reference
When NOT to Use This Skill
- Logging configuration - Use logging skills
- Exception handling patterns - Use language-specific skills
- Security error handling - Use security skills
Deep Knowledge: Use
mcp__documentation__fetch_docsfor framework-specific error handling patterns.
Standard Error Response Structure
RFC 7807 (Problem Details)
{
"type": "https://api.example.com/errors/validation",
"title": "Validation Error",
"status": 400,
"detail": "The request body contains invalid data",
"instance": "/users/123",
"errors": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
}
],
"traceId": "abc123-def456"
}
Simple Error Structure
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request body contains invalid data",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
}
OpenAPI Error Definition
components:
schemas:
Error:
type: object
required:
- code
- message
properties:
code:
type: string
description: Machine-readable error code
example: "VALIDATION_ERROR"
message:
type: string
description: Human-readable error message
example: "Invalid request data"
details:
type: array
items:
$ref: '#/components/schemas/ErrorDetail'
traceId:
type: string
description: Request trace ID for debugging
example: "abc123-def456"
ErrorDetail:
type: object
properties:
field:
type: string
example: "email"
message:
type: string
example: "Invalid email format"
code:
type: string
example: "INVALID_FORMAT"
responses:
BadRequest:
description: Invalid request data
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: "VALIDATION_ERROR"
message: "Invalid request data"
details:
- field: "email"
message: "Invalid email format"
Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: "UNAUTHORIZED"
message: "Authentication required"
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: "NOT_FOUND"
message: "User not found"
Status Code Mapping
| Status | Code Constant | When to Use |
|--------|---------------|-------------|
| 400 | VALIDATION_ERROR | Invalid request body/params |
| 401 | UNAUTHORIZED | Missing or invalid auth |
| 403 | FORBIDDEN | Valid auth, insufficient permissions |
| 404 | NOT_FOUND | Resource doesn't exist |
| 409 | CONFLICT | Duplicate resource, state conflict |
| 422 | UNPROCESSABLE_ENTITY | Semantic validation failure |
| 429 | TOO_MANY_REQUESTS | Rate limit exceeded |
| 500 | INTERNAL_ERROR | Unexpected server error |
| 503 | SERVICE_UNAVAILABLE | Temporary unavailability |
Frontend Error Handling
TypeScript Error Types
// Base error interface matching backend
interface ApiError {
code: string;
message: string;
details?: ErrorDetail[];
traceId?: string;
}
interface ErrorDetail {
field: string;
message: string;
code?: string;
}
// Error class for typed handling
class ApiRequestError extends Error {
constructor(
public readonly code: string,
message: string,
public readonly status: number,
public readonly details?: ErrorDetail[],
public readonly traceId?: string,
) {
super(message);
this.name = 'ApiRequestError';
}
static fromResponse(status: number, body: ApiError): ApiRequestError {
return new ApiRequestError(
body.code,
body.message,
status,
body.details,
body.traceId,
);
}
isValidationError(): boolean {
return this.code === 'VALIDATION_ERROR';
}
isNotFound(): boolean {
return this.code === 'NOT_FOUND';
}
isUnauthorized(): boolean {
return this.code === 'UNAUTHORIZED';
}
getFieldError(field: string): string | undefined {
return this.details?.find(d => d.field === field)?.message;
}
}
Fetch Wrapper with Error Handling
async function fetchApi<T>(
url: string,
options?: RequestInit,
): Promise<T> {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
const errorBody: ApiError = await response.json();
throw ApiRequestError.fromResponse(response.status, errorBody);
}
return response.json();
}
// Usage
try {
const user = await fetchApi<User>('/api/users/123');
} catch (error) {
if (error instanceof ApiRequestError) {
if (error.isNotFound()) {
showNotification('User not found');
} else if (error.isValidationError()) {
setFormErrors(error.details);
} else {
showNotification(error.message);
}
}
}
React Error Handling
// Error boundary for unexpected errors
class ErrorBoundary extends React.Component<Props, State> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
// Form validation errors
function UserForm() {
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const mutation = useMutation({
mutationFn: createUser,
onError: (error) => {
if (error instanceof ApiRequestError && error.isValidationError()) {
const errors: Record<string, string> = {};
error.details?.forEach(detail => {
errors[detail.field] = detail.message;
});
setFieldErrors(errors);
} else {
toast.error(error.message);
}
},
});
return (
<form onSubmit={handleSubmit}>
<input name="email" />
{fieldErrors.email && <span className="error">{fieldErrors.email}</span>}
</form>
);
}
React Query Error Handling
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// Don't retry on client errors
if (error instanceof ApiRequestError && error.status < 500) {
return false;
}
return failureCount < 3;
},
},
mutations: {
onError: (error) => {
// Global error handler
if (error instanceof ApiRequestError) {
if (error.isUnauthorized()) {
window.location.href = '/login';
}
}
},
},
},
});
Backend Error Implementation
NestJS
// Error filter
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = 500;
let errorResponse: ApiError = {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
traceId: request.headers['x-trace-id'] as string,
};
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'object') {
errorResponse = {
...errorResponse,
...(exceptionResponse as object),
};
}
}
response.status(status).json(errorResponse);
}
}
// Custom exceptions
export class ValidationException extends HttpException {
constructor(details: ErrorDetail[]) {
super(
{
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details,
},
HttpStatus.BAD_REQUEST,
);
}
}
Spring Boot
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidation(
MethodArgumentNotValidException ex) {
List<ErrorDetail> details = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new ErrorDetail(
error.getField(),
error.getDefaultMessage(),
error.getCode()
))
.toList();
ApiError error = new ApiError(
"VALIDATION_ERROR",
"Validation failed",
details
);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(
ResourceNotFoundException ex) {
ApiError error = new ApiError(
"NOT_FOUND",
ex.getMessage()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
Validation Report Template
## Error Contract Validation
### Error Response Structure
| Field | Backend | Frontend Handles | Status |
|-------|---------|------------------|--------|
| code | Present | Parsed | OK |
| message | Present | Displayed | OK |
| details | Present | Form errors | OK |
| traceId | Present | Not used | WARNING |
### Status Code Handling
| Status | Backend Returns | Frontend Handles | Status |
|--------|-----------------|------------------|--------|
| 400 | VALIDATION_ERROR | Shows form errors | OK |
| 401 | UNAUTHORIZED | Redirects to login | OK |
| 403 | FORBIDDEN | Shows permission error | OK |
| 404 | NOT_FOUND | Shows not found page | OK |
| 500 | INTERNAL_ERROR | Shows generic error | OK |
### Recommendations
1. Frontend should log traceId for debugging
2. Add retry logic for 503 responses
Anti-Patterns
| Anti-Pattern | Why It's Bad | Correct Approach | |--------------|--------------|------------------| | Inconsistent error structure | Hard to parse | Use standard format | | Stack traces in production | Security risk | Use error codes only | | Generic "Error occurred" | Poor UX | Specific messages | | No error codes | Hard to handle | Add machine-readable codes | | Swallowing errors silently | Hidden failures | Log and notify |
Quick Troubleshooting
| Issue | Likely Cause | Solution |
|-------|--------------|----------|
| Error not parsed | Wrong Content-Type | Check application/json |
| Missing field errors | Different structure | Align error schema |
| No trace ID | Not generated | Add trace ID middleware |
| Wrong status code | Backend misconfiguration | Check exception handlers |
| Error message empty | Serialization issue | Check DTO annotations |
微信扫一扫