How I Handle Logs and Errors in My SaaS Projects
When you’re running a SaaS — even a small one — logs and error handling can make or break your sanity. If something fails silently, you’re blind. If everything logs too much, you’re buried. Over time, I’ve built a simple, consistent system that gives me visibility without chaos.
The Goal: Context Without Noise The key is balance. You need enough context to understand what happened — but not so much that logs become unreadable. So for every major operation, I log:
- The user or process that triggered it
- What it tried to do
- Whether it succeeded or failed
Example:
console.info(`[Backup Created] user:${userId} scenario:${scenarioId}`)
If something goes wrong:
console.error(`[Backup Failed] user:${userId} scenario:${scenarioId}`, error)
Readable. Searchable. Easy to grep.
Separate Logs From Errors I treat logs and errors differently:
- Logs = expected events
- Errors = unexpected events
That distinction helps me spot real issues faster. I don’t want to scroll through hundreds of “normal” logs to find one broken API call. In production, I filter or route them separately — for example, sending error level events to monitoring while keeping info logs lightweight.
Centralized Logging I use a consistent logging utility stored in src/lib/log.ts.
export function logInfo(message: string, meta?: any) {
console.info(`[INFO] ${message}`, meta ?? "")
}
export function logError(message: string, error?: any) {
console.error(`[ERROR] ${message}`, error ?? "")
}
This way, I can replace console later with a more advanced service (like Logtail, Sentry, or AWS CloudWatch) without touching the rest of my code. Centralizing logging logic gives flexibility — I can scale visibility later without rewriting everything.
Error Boundaries on the Frontend For client-side errors, I always wrap major areas in React error boundaries. It’s simple: if something crashes, show a fallback instead of a white screen.
<ErrorBoundary fallback={<ErrorMessage />}>
<Dashboard />
</ErrorBoundary>
It’s better UX, and I can still send the error to Sentry or a custom handler.
Catching Errors on the Server Server-side, I use try/catch in all async functions:
try {
await doSomethingCritical()
} catch (error) {
logError("Critical operation failed", error)
throw new Error("Operation failed")
}
The user gets a clean message; I get the stack trace. If it’s something that shouldn’t ever happen, I send it to an external alert — so I can fix it before a user even reports it.
Minimalism Scales I don’t need a complex setup for 10 users, but I build as if I might have 10,000. So, my system is simple, but ready to grow:
- Centralized logging functions
- Separate log and error levels
- Graceful frontend fallbacks
- Alert hooks for serious issues
No giant monitoring dashboards — just clarity and consistency.
Wrap-Up Good error handling isn’t about tools — it’s about discipline.
- Log what matters
- Separate normal from broken
- Store your logic in one place
- Make it easy to upgrade later
The result: calm debugging, faster fixes, and no 3 a.m. panic scrolls through unreadable logs.
Read the full article here: https://medium.com/@joseph.goins/how-i-handle-logs-and-errors-in-my-saas-projects-95c23075b54b