The Silent Killers in Your Go App: Unhandled Errors
Do you know what’s scarier than a panic in a Golang application? 🤔 An error that doesn’t panic, doesn’t log, doesn’t show up — but silently fails in production.
These are like landmines scattered across your codebase, patiently waiting to blow up at the worst possible time — like when you’re sipping on a cold beer 🍺, celebrating a successful release.
And then…
📞 “Hey, the feature isn’t working. Logs look clean. What’s going on?”
Oh no. That’s your PM. On a weekend. With no error trace. I’ve been there. And yes, I left my beer mid-sip. 😞
When I first started with Go, I didn’t fully grasp that errors in Go are first-class citizens — passed around just like any other value. If you don’t handle them, they don’t scream. They whisper. And that whisper can turn into a production outage.
So this article is not just a rant. It’s a survival guide — to help you prevent these ghost errors from ruining your weekend or your system’s reputation. 🧯
Let’s walk through:
How to structure your error objects for better debugging
How to distinguish between expected and unexpected errors
And how to turn unhandled errors into actionable alerts for your team
Why Error Handling in Go Needs Your Full Attention
Not all errors are created equal. Some are edge cases you anticipate. Some are wild cards. But all of them need structure and context if you want to trace them later.
✅ Expected (or edge-case) errors:
User requests a resource that doesn’t exist
Invalid query parameter
Authorization failure
Timeout or network delay
These are normal. They happen. You handle them, maybe return a or , and move on.
❌ Unexpected errors (the real troublemakers):
DB connection fails
Memory exhaustion
Filesystem full
Internal services misbehaving
Panic due to nil pointer dereference
These should never happen — but if they do, you want to know immediately.
What Should Your Error Object Include?
Think of your error like a black box recorder. When things go wrong, it should tell you exactly what happened and how to reproduce it. Here’s what it must include:
🧩 What happened? A clear description: e.g. “invalid DB credentials”, “failed to marshal JSON”, or “timeout while calling payment gateway”.
📍 Where did it happen? Add stack traces or function names — Go makes this easy with tools like runtime.Caller() or libraries like pkg.errors or runtime/debug.
⏱️ When did it happen? Include request context: user ID, request ID, trace ID, timestamp — all of these help recreate the scenario and debug faster.
💬 What should the user see? Don’t leak internals! Instead, return friendly, actionable messages:
“Something went wrong. Please get in touch with support with Reference ID: xyz123.”
🔍 How can the devs debug it further? Include traceable metadata in logs or bug tracking systems so your team has breadcrumbs to follow. Reference IDS go a long way.
In the next section, we’ll build a real REST API example and demonstrate how to put this into practice, gracefully handling, categorising, and propagating errors up to monitoring and alerting systems.
But before that, just remember this: Handling errors isn’t just about avoiding panics — it’s about staying in control of your system even when it misbehaves.
Building a REST API in Go (Without Losing Your Mind Over Errors)
So, imagine we’re building a REST API server — nothing fancy, just your good old layered Go backend:
Router Layer: Parses the HTTP request and routes it appropriately.
Service Layer: Decides whether to hit the DB, cache, or file system.
Persistence Layer: Deals directly with the database.
Let’s zoom into the persistence layer, where two common issues can ruin your day:
The database isn’t reachable (classic).
The data just… doesn’t exist (also classic).
For the second case, we create a custom error structure to wrap all the “not found” drama:
🧠 What’s Cool About This?
Error() gives a user-friendly summary.
LogMessage() gives devs all the gritty details.
ObjectType tells us what was missing.
ObjectIdentifiers explains how we looked for it.
StackTrace points us right to where the problem happened.
Now, let’s see this in action in the persistence layer:
Breakdown
1️⃣ We forward the DB error if it happens.
2️⃣ We simulate an invalid user ID scenario (I know validation doesn’t belong here, but bear with me — this is just for the demo).
A small disclaimer: I know this is not the right place for this validation, but I have only done this to explain how to gracefully propagate the error from the innermost layer.
Now Let’s Head Over to the Service Layer 🚪
Here’s where we catch and convert those deep, scary errors into something the client can actually digest.
We also define a handy interface:
Now, here’s the service logic in action:
🧪 What’s Happening Here?
Interface Check: We see if the error implements ILogMessageError — if yes, it’s a known error.
WrapError: Transforms the error into a format with HTTP status and message.
logBug(): If it’s an unexpected error, alert the engineering team. PagerDuty bells ring. People panic. You know the drill.
Finally, The Router — The Gatekeeper to the Client 🌐
Here, we check whether the service-level error can be converted to an HTTP response.
🧩 Why This Works
Handled errors bubble up in a predictable format with HTTP codes.
Unhandled errors raise the flag to devs and get returned as generic 500s.
Clients stay informed. Devs stay sane. Production stays alive. 😅
🎯 Final Thoughts
And there you have it — an end-to-end error handling strategy that:
Keeps your layers clean and decoupled
Makes debugging a breeze with structured error logs
Ensures your users never see a dreaded “500 Internal Server Error” without context
🔗 Full source code available on GitHub: 👉 architagr/The-Weekly-Golang-Journal — error-propagation
Stay Connected!
💡 Follow me on LinkedIn: Archit Agarwal
🎥 Subscribe to my YouTube: The Exception Handler
📬 Sign up for my newsletter: The Weekly Golang Journal
✍️ Follow me on Medium: @architagr
👨💻 Join my subreddit: r/GolangJournal
API Architect at IndiaPost
3moWhen it comes APIs..Unhandled exceptions are better handled in fiber and echo becoz of centralised error handling capability