# How do I set a clear rule for when to log at WARN versus ERROR?

> Log at ERROR when the line would wake someone at 3am and at WARN when it would not, log a failure once at its boundary, and alert on the ERROR rate.

- Asked: 2026-07-21
- Answered: 2026-07-24
- Asked by: Can
- Tags: observability, logging
- Source: https://www.muhammetsafak.com.tr/en/just-ask/how-do-i-set-a-clear-rule-for-when-to-log/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** Our Laravel APIs and Go services log almost everything at ERROR level — caught exceptions, failed validations, retried HTTP calls, even expected 404s all land as ERROR.

As a result our Sentry and alert channels have turned into pure noise; when a real problem happens it drowns among thousands of meaningless ERRORs. How do I draw a clear, applicable line between WARN and ERROR that the whole team can follow consistently?


Short answer: the level should be driven by "does a human need to act?", not by "what the log line is about".

## Short answer

ERROR = a promise you broke; WARN = something unexpected that the system recovered from on its own. In other words, let the outcome pick the level, not the call site. There's a volume side to the noise too; I covered the scenario where logs fill the disk and turn into an outage in [the log storm answer](/en/just-ask/preventing-log-storms-and-disk-full-outages/).

## Why

1. **Actionability is the axis.** ERROR is when a request or job failed because of your fault and someone needs to look. WARN is something unexpected but tolerated: the retry succeeded, a fallback kicked in, a soft limit was approached. If your answer to "if this line fires at 3am, would I need to wake someone?" is yes, it's ERROR.
2. **An expected failure isn't a bug.** A 404, a validation error, a user's bad input — those aren't your code breaking; they're INFO or, at most, WARN. Rough rule: 4xx is the client's problem, 5xx is yours. Reserve ERROR for when your own code failed to keep its promise.
3. **Retries and fallbacks produce different outcomes.** A single failed attempt that will be retried is WARN; a transient blip isn't actionable. But once all retries are exhausted or the message lands in the dead-letter queue, it's ERROR — because the work is permanently unfinished.
4. **ERROR and FATAL/critical aren't the same thing either.** FATAL is when the process can't continue (missing config, failed boot); ERROR is a request/job failure while the process stays up.

## What to do

1. **Alert on ERROR only, and by rate.** Let your alerts trigger off ERROR (and fatal), and even then on a threshold/rate ("50 ERRORs in 5 minutes"), not on a single line. WARN feeds dashboards, not pagers.
2. **Don't log the same failure five times.** In Go, if every layer both returns and logs the error, one failure becomes five lines. Log once at the boundary where the outcome is decided (the handler); lower layers should just wrap and return the error. In Laravel, do the same by using the `report()` level in the exception handler instead of sprinkling `Log::error` everywhere.

   ```go
   if err := publish(ctx, msg); err != nil {
       if attempt < maxRetries {
           log.Warn("publish failed, will retry", "attempt", attempt, "err", err)
           return retry
       }
       // retries exhausted: now it's genuinely actionable
       log.Error("publish failed permanently, sent to DLQ", "err", err)
   }
   ```

3. **Make every ERROR line actionable.** Every ERROR line should carry enough context to assume someone will look at it in the middle of the night: request/trace id, the relevant identifiers, and the wrapped error chain. An ERROR nobody can act on isn't really an ERROR.

**Bottom line:** personally I'd give the team a one-sentence rule: "If this log firing at 3am means someone must wake up, it's ERROR; otherwise WARN." On top of that I'd enforce logging a failure only once, at its boundary, and wire alerts to the ERROR rate. Those two changes dissolve most of the noise on their own; you clean up the rest over a few weeks by re-leveling the misclassified lines. The goal isn't zero ERRORs — it's that every ERROR actually means something.

## Related Reading

- [How should I carry a correlation ID from the HTTP request all the way into queued jobs?](https://www.muhammetsafak.com.tr/en/just-ask/how-should-i-carry-a-correlation-id-from-the-http-request/) — Just Ask
- [How do I design a self-healing deploy that auto-rolls back when the error rate spikes?](https://www.muhammetsafak.com.tr/en/just-ask/automated-rollback-on-error-spikes-self-healing-deploys/) — Just Ask
- [How do I prevent log storms and disk-full outages?](https://www.muhammetsafak.com.tr/en/just-ask/preventing-log-storms-and-disk-full-outages/) — Just Ask
