DEV Community

Cover image for 🚨 Go Devs, Are You Really Checking Error Types Correctly? (Or Just Pretending Like I Did)
Archit Agarwal
Archit Agarwal

Posted on • Edited on

🚨 Go Devs, Are You Really Checking Error Types Correctly? (Or Just Pretending Like I Did)

Spoiler: If you’ve ever said “This should never happen” before reading the logs—welcome to the club, friend. Let me show you what I learned the hard way.

The Incident That Triggered My Existential Crisis (a.k.a QA Caught It Again 😤)

There I was, feeling like the king of the backend. I’d just finished wiring up login logic for our Go-based API. It was clean. It was fast. It had tests. And yes, I even sprinkled in some fmt.Errorf() wrapping for extra context. You know, the kind that impresses seniors during code reviews.

And then... QA logs in.

“Hey, the wrong status code is being returned when users enter the wrong credentials. It says internal server error, not unauthorised.”

I blink.

“That’s not supposed to happen.” – me, a developer, every Tuesday.

I dig in. Logs? Present. Errors? Being thrown. Status codes? Totally wrong. And then it hit me like a rogue nil pointer on a Friday deploy...

I WAS CHECKING ERROR TYPES INCORRECTLY.

Why This Happens: Go’s Error Wrapping Will Outsmart You (If You Let It)

Let’s rewind. Say you’ve got a nice clean separation of concerns:

  • Persistence layer returns apperrors.NotFound
  • Service layer wraps it with extra info using fmt.Errorf("%w", err)
  • Router layer checks the error type and returns the right HTTP code Simple, right?

Here’s what that might look like:

func (p ArticlePersistence) Get(id int) (*entities.Article, error) {
 if id == 1 {
  return &entities.Article{
   Id:          1,
   Title:       "Article 1",
   Description: "Article 1 description",
   CreatedAt:   time.Now().Add(-10 * time.Hour),
   CreatedBy:   1,
  }, nil
 }
 return nil, apperrors.NotFound{Id: id}
}
Enter fullscreen mode Exit fullscreen mode

And in the service:

func (svc ArticleService) Get(id int) (*dto.Article, error) {
 userInfo, err := svc.articlePersistenceObj.Get(id)
 if err != nil {
  return nil, fmt.Errorf("%w, entity that is not found is article", err)
 }
 return (&dto.Article{}).Init(userInfo), nil
}
Enter fullscreen mode Exit fullscreen mode

Looks good, right? Now let’s jump to login land, where all hell breaks loose.

The Code That Almost Worked (but Totally Didn't)

Picture this: your Go API allows login, but blocks users with active sessions. You want to return:

  • 401 Unauthorized for bad credentials
  • 412 Precondition Failed for already logged-in users Here’s the setup:
func (svc LoginPersistence) AuthenticateUser(authReq *dto.AuthRequest) (*entities.User, error) {
 if authReq.UserName == "user2" && authReq.Password == "password2" {
  return &entities.User{}, nil
 } else if authReq.UserName == "user3" && authReq.Password == "password3" {
  return &entities.User{}, nil
 }
 return nil, apperrors.CredentialError{}
}
Enter fullscreen mode Exit fullscreen mode

And the service logic:

func (svc LoginService) AuthenticateUser(authReq *dto.AuthRequest) (*dto.AuthResponse, error) {
 userInfo, err := svc.loginPersistenceObj.AuthenticateUser(authReq)
 if err != nil {
  return nil, fmt.Errorf("request (%+v), %w", authReq, err)
 }
 activeSessions := userInfo.Sessions.Filter(func(val *entities.Session) bool {
  return val.EndTime.IsZero()
 })
 if len(activeSessions) > 0 {
  return nil, apperrors.ActiveSessionError{}
 }
 return (&dto.AuthResponse{}).Init(userInfo), nil
}
Enter fullscreen mode Exit fullscreen mode

But now the router is like:

switch err := err.(type) {
 case apperrors.CredentialError:
   c.JSON(http.StatusUnauthorized, ...)

Enter fullscreen mode Exit fullscreen mode

And... 🛑 Boom. It never matches. Because wrapped errors don’t match direct type assertions. 🤦

The Solution: errors.As to the Rescue 🦸‍♂️

Turns out, Go already thought of this and gave us a tool: errors.As. It’s the try-catch-finally you never knew you had.

Here’s how you do it the right way:

func (r LoginRouter) AuthenticateV2(c *gin.Context) {
 var authReq dto.AuthRequest
 if err := c.ShouldBindJSON(&authReq); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  return
 }

 userInfo, err := r.loginServiceObj.AuthenticateUser(&authReq)
 if err != nil {
  log.Println(err)
  var credErr apperrors.CredentialError
  var sessionErr apperrors.ActiveSessionError

  if errors.As(err, &credErr) {
   c.JSON(http.StatusUnauthorized, gin.H{"error": credErr.Error()})
  } else if errors.As(err, &sessionErr) {
   c.JSON(http.StatusPreconditionFailed, gin.H{"error": sessionErr.Error()})
  } else {
   c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  }
  return
 }

 c.JSON(http.StatusOK, userInfo)
}
Enter fullscreen mode Exit fullscreen mode

💡 Pro Tip: errors.As walks the error chain and tells you, “Yes, this is a CredentialError… just wrapped in three layers of sarcasm and context.”

Final Thoughts: Don't Let Errors Gaslight You

If you’re coming from Java, .NET, or TypeScript, you're used to catch blocks and exceptions behaving. In Go, error handling is a manual process, and that includes type-checking.

If you’re not using errors.As, you’re basically saying:

“I don't care if the house is on fire, I’ll just check if the door feels warm.”
So here’s your checklist:

✅ Wrap errors with %w for context
✅ Use errors.As() when matching error types
✅ Don’t type-assert directly on wrapped errors
✅ Log meaningful context — future you will thank you
✅ Bookmark this article for your next QA fight 🧠

Ready to Handle Errors Like a Pro?

Explore the full example in the GitHub repo. Try refactoring your current project with errors.As. And next time QA says, “It’s not working,” just smile knowingly… and ship a fix before they finish the sentence 😎

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
👨‍💻 Follow me on twitter: @architagr

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more