| name | error-handling-logging |
| description | Error handling, logging, and observability patterns for this monorepo. Use when implementing exception filters, error boundaries, structured logging, CloudWatch integration, or health checks. Triggers on tasks involving error responses, exception handling, log formats, monitoring, health endpoints, or observability. |
| frameworks | ["laravel","nextjs","cloudwatch"] |
| languages | ["php","typescript"] |
| category | observability |
| updated | "2026-04-29T00:00:00.000Z" |
Error Handling & Logging
Quick Reference
| Layer | Error Handling | Location |
|---|
| Laravel global | Exception handler | apps/laravel/app/Exceptions/Handler.php |
| Laravel validation | Form Request (422) | apps/laravel/app/Http/Requests/ |
| Frontend queries | TanStack Query error states | Per-feature query hooks |
| Frontend forms | React form error states | Per-feature form components |
| Production logs | CloudWatch Logs | /ec2/{PROJECT_NAME}-{service} |
| Health checks | /api/v1/health endpoint | apps/laravel/routes/api.php |
Backend Error Handling (Laravel)
Standard Error Response Format
Laravel returns JSON errors automatically when the request expects JSON:
{
"message": "The title field is required.",
"errors": { "title": ["The title field is required."] }
}
{ "message": "Unauthenticated." }
{ "message": "This action is unauthorized." }
{ "message": "No query results for model [Todo] 1" }
Common Error Patterns
abort(404, 'Todo not found');
$todo = Todo::findOrFail($id);
$this->authorize('delete', $todo);
abort(409, 'A todo with this title already exists');
Registering Global Providers
@Module({
providers: [
{ provide: APP_PIPE, useClass: ZodValidationPipe },
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
],
})
export class AppModule {}
Order matters: Pipe runs first (validates input) → Handler runs → Interceptor runs (validates output) → Filter catches any exceptions.
Frontend Error Handling
Query Error States
import { useTodosQuery } from "@/features/todos/api/todos.hooks"
import { Alert, AlertDescription } from "@/core/components/ui/alert"
export function TodosList() {
const { data: todos, isLoading, error } = useTodosQuery()
if (isLoading) {
return <Spinner />
}
if (error) {
return (
<Alert variant="destructive">
<AlertDescription>
Failed to load todos. Please try again.
</AlertDescription>
</Alert>
)
}
if (!todos?.length) {
return <p className="text-muted-foreground">No todos yet.</p>
}
return <ul>{todos.map(todo => <TodoCard key={todo.id} todo={todo} />)}</ul>
}
Pattern: Always handle three states: loading, error, and empty data.
Mutation Error Handling
const createMutation = useCreateTodoMutation()
async function handleSubmit(data: CreateTodoInput) {
try {
await createMutation.mutateAsync(data)
toast.success("Todo created!")
} catch (error) {
toast.error("Failed to create todo. Please try again.")
}
}
<Button disabled={createMutation.isPending}>
{createMutation.isPending ? "Creating..." : "Create"}
</Button>
{createMutation.error && (
<p className="text-destructive text-sm">
{createMutation.error.message}
</p>
)}
Form Validation Errors
TanStack Form surfaces validation errors per field:
<form.Field name="title">
{(field) => (
<div>
<Input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.errors?.length > 0 && (
<p className="text-destructive text-sm">
{field.state.meta.errors.join(", ")}
</p>
)}
</div>
)}
</form.Field>
Health Checks
Laravel Health Endpoint
The /api/v1/health endpoint is critical for:
- Docker HEALTHCHECK directives
- EC2 ALB target group health checks
Expected response:
{ "status": "ok" }
Health Check Configuration
| Platform | Endpoint | Interval | Timeout | Retries | Start Period |
|---|
| Docker | localhost:80/api/v1/health | 30s | 10s | 3 | 40s |
| ALB | /api/v1/health (200 OK) | 30s | 10s | — | — |
Production Logging (CloudWatch)
Log Groups
| Service | Log Group | Retention |
|---|
| Web | /ec2/{PROJECT_NAME}-web-{environment} | 7 days |
| API | /ec2/{PROJECT_NAME}-api-{environment} | 7 days |
CloudWatch Agent Configuration (EC2)
EC2 instances use the CloudWatch agent to ship Docker container logs. Container stdout/stderr is automatically captured from Docker.
Structured Logging Best Practices (Laravel)
Log::error('Failed to create todo', [
'user_id' => $request->user()->id,
'input' => $request->validated(),
'error' => $e->getMessage(),
]);
Log::error('Error: ' . $e->getMessage());
CloudWatch Alarms
| Alarm | Condition | Action |
|---|
| CPU > 80% | 2 periods of 5 min | SNS notification |
| Memory > 80% | 2 periods of 5 min | SNS notification |
| Unhealthy instances > 0 | 1 period of 60s | SNS notification |
| 5xx errors > 50 | 5 periods of 60s | SNS notification |
| 4xx errors > 100 | 5 periods of 60s | SNS notification |
Error Handling Rules
- Backend: Always use HttpException subclasses —
NotFoundException, BadRequestException, ForbiddenException, etc.
- Never expose stack traces — The filter strips them automatically
- Log context, not secrets — Never log
DATABASE_URL, session tokens, or passwords
- Frontend: Handle all three states — loading, error, empty for every query
- Mutations: Show feedback — Toast on success, error message on failure
- Health checks: Keep lightweight — Quick DB ping, no heavy operations
- Production: Use CloudWatch — All console output routes to CloudWatch via ECS log driver