DEV Community

Clavin June
Clavin June

Posted on • Originally published at clavinjune.dev on

Creating Taboo Error Handler For Go

Photo by @hhh13 on Unsplash

I created this module for Golang to help me with error handling. This idea showed when a colleague of mine was thinking to pass context from handler to service, to repository to trace the error log more verbose. I disagreed with him because I thought that’s not what context is for. Perhaps I was wrong or he was wrong, or maybe both of us wrong because this is our first Golang project that deployed on production.

Despite our opinion about context, we both agreed that Golang’s error handling is too verbose and bulky. It makes us read more error handling more than read the system flow itself. Then I remember when I was coding using Java/Kotlin that I always use throws, throw, and try-catch block to handle any errors.

Hmm…

Why don’t I create it for Golang?

Then I create this try-catch block module for Golang called taboo. Because I know this thing creates polemic among Golang developer but, then I thought why don’t I give it a try?

For the design itself, I was inspired by this article but the whole implementation is all adjusted with my current needs. Instead of error, this module is based on panic and recover so it is quite dangerous whenever used in the wrong condition.

Let’s take an example:

package main

func div(a, b int) int {
  return a / b
}

func main() {
  div(10, 0)
}

Enter fullscreen mode Exit fullscreen mode

When b is filled with zero, it causes panic

panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.div(...)
        /tmp/anon-org/taboo/cmd/example.go:4
main.main()
        /tmp/anon-org/taboo/cmd/example.go:8 +0x12

Process finished with exit code 2

Enter fullscreen mode Exit fullscreen mode

So, we can handle it using taboo like this:

taboo.Try(func() {
  div(10, 0)  
}).Catch(func(e *taboo.Exception) {
  fmt.Println(e.Error())
}).Do()

Enter fullscreen mode Exit fullscreen mode

taboo will catch the panic and try to recover it and make a stack of error called taboo.Exception to trace error more verbose. So the program ends like this:

main.div:9 runtime error: integer divide by zero

Process finished with exit code 0

Enter fullscreen mode Exit fullscreen mode

Quite handy right?

Then, what if I want to throw or rethrow the error to the first caller?

package main

import (
  "errors"
  "fmt"
  "github.com/anon-org/taboo/pkg/taboo"
)

func div(a, b int) int {
  if b == 0 {
    taboo.Throw(errors.New("division by zero detected"))
  }
  return a / b
}

func callDiv() int {
  var result int

  taboo.Try(func() {
    result = div(10, 0)
  }).Catch(func(e *taboo.Exception) {
    e.Throw("callDiv rethrow this error")
  }).Do()

  return result
}

func callCallDiv() int {
  var result int

  taboo.Try(func() {
    result = callDiv()
  }).Catch(func(e *taboo.Exception) {
    e.Throw("callCallDiv rethrow this error")
  }).Do()

  return result
}

func main() {
  taboo.Try(func() {
    callCallDiv()
  }).Catch(func(e *taboo.Exception) {
    fmt.Println(e.Error())
  }).Do()
}

Enter fullscreen mode Exit fullscreen mode

e.Throw(message) Will wrap the previous exception, and throw it again to the previous caller. So the printed error will be like this:

main.callCallDiv:34 callCallDiv rethrow this error caused by:
  main.callDiv:22 callDiv rethrow this error caused by:
    main.div:11 division by zero detected

Process finished with exit code 0

Enter fullscreen mode Exit fullscreen mode

It just like try-catch block in Java/Kotlin I think but with many flaws lol. This module is still an experiment that I myself is not going to using this module in production. Or perhaps should I?

Top comments (2)

Collapse
 
kunde21 profile image
Chad Kunde • Edited

This is a common pattern from devs coming from Java/C++/C#/Python/etc. It's not a problem of language design, it's a conflict of expected pattern. The go pattern ends up being fewer lines of code (45 vs 36), even if it handles the error at the top of the call chain instead of the bottom:

package main

import (
  "errors"
  "fmt"
)

func div(a, b int) (int, error) {
  if b == 0 {
    return 0, errors.New("division by zero detected")
  }
  return a / b, nil
}

func callDiv() int {
  result, err = div(10, 0)
  if err != nil {
    return 0, errors.Newf("div error %v", err)
  }
  return result, nil
}

func callCallDiv() (int, error) {
  result, err := callDiv()
  if err != nil {
    return 0, errors.Newf("callCallDiv error %v", err)
  }
  return result, nil
}

func main() {
  c, err := callCallDiv()
  if err != nil {
    fmt.Println(e.Error())
  }
}
Enter fullscreen mode Exit fullscreen mode

The different paradigms from the languages cause us to assume things that aren't necessarily true. Go-style error handling in Java/Python is incredibly verbose, because it's more thorough than the Java/Python pattern. That's neither good nor bad, it's simply a difference of expectations.

Exceptions in Java/Python are considered catastrophic until handled (the infamous "unhandled exception error"), while Go considers them routine until promoted to an exception (panic).

Collapse
 
clavinjune profile image
Clavin June • Edited

yes, I totally agree with you, because it's natural to be verbose for Go. But since using panic could stop the program itself, it is quite dangerous if the user missed 1 wrapper unless you can provide panicHandler.