Best Practices for Error Handling in Go APIs

435 views
Skip to first unread message

Byungjun You

unread,
Feb 3, 2025, 6:01:59 AMFeb 3
to golang-nuts

In Java, checked exceptions allow developers to define in advance which exceptions a function can throw. However, it seems that Go does not have such a feature. Would it be considered a best practice in Go to document the possible errors that an API function can return? Additionally, what are some best practices that API providers can follow to help API users handle errors effectively?

Mike Schinkel

unread,
Feb 3, 2025, 1:02:37 PMFeb 3
to Byungjun You, GoLang Nuts Mailing List
On Feb 3, 2025, at 6:01 AM, Byungjun You <schi...@gmail.com> wrote:

In Java, checked exceptions allow developers to define in advance which exceptions a function can throw. However, it seems that Go does not have such a feature. Would it be considered a best practice in Go to document the possible errors that an API function can return? Additionally, what are some best practices that API providers can follow to help API users handle errors effectively?

In Go, errors are values[1] meaning most in the Go community frown on try-catch style exception handling as in Java. 

Each function in Go which can generate an error will typically return that error as an additional value; e.g.:

func (d Data) getValue() (value any, err error) {
   if d.value == nil {
      return nil, errors.New("value as not been set")
   }
   if reflect.ValueOf(d.value).IsZero() {
      return nil, errors.New("value is empty")
   }
   return value,err
}

Then getValue() would be called like this:

value, err := data.getValue()

See https://go.dev/play/p/PciRk_t_UaV for full working example.

In some cases — though not as many as I would like — Go developers often create sentinel values that might look like this:

var (
  ErrValueNotSet  = errors.New("value as not been set")
  ErrValueIsEmpty = errors.New("value is empty")
)

Then callers can check them using errors.Is() like so:

  if errors.Is(err, ErrValueNotSet) {...}
  if errors.Is(err, ErrValueIsEmpty) {...}

See https://go.dev/play/p/pamY1pcAoVP for a full working example.

A common sentinel error value from the Go std lib is io.EOF[2]

However, AFAIK, there is no way programmatically to determine which errors a Go function might return at this time. It would be nice if there were some built-in mechanism for doing that using the reflect package, but I would not expect that to be added as I cannot imagine how it could be implemented without slowing down compilation. Unfortunately.

As for best practices, IMO that would be to define sentinel error variables instead of errors created inline with errors.New() or fmt.Errorf() for all errors that your function may return, and then use errors.Join() to join with any errors returned by a function your function called before returning to the caller. Then document your sentinel error variables and commit to not renaming or removing them in future versions of your API.

Does that answer your question?

-Mike

Jeremy French

unread,
Feb 3, 2025, 8:40:49 PMFeb 3
to golang-nuts
I don't know if it qualifies as "best practice" but I think it would certainly qualify as convention to put the error as the last returned value.  It is also extremely common to name the error "err", though that would have little impact on API consumers as they can rename it whatever they want. 

It's also quite common at this point to wrap your errors with fmt.Errorf(), which allows errors.Is() to match the root error, or anywhere up the error chain, which can be convenient for your consumers. But I think I would dare to say that it's best practice to only wrap an error if you have something meaningful to add to it.  For example "no such file or directory" is less helpful than "config: no such file or directory." but "can't find file: no such file or directory" doesn't add anything.  I would also say that it's (somewhat) considered best practice to keep your errors terse and written for the developers, rather than flowery descriptive text written for the end user.  If you need text for the end user, translate it at the point of intercepting the error and doing something with it.

And of course, I believe Effective Go has a bit to say on the subject.

Darren Grant

unread,
Feb 4, 2025, 4:59:25 PMFeb 4
to golang-nuts
What's the current best practice for getting a stack trace from an error passed up from further down in the stack?

E.g. a web server middleware uses recover to catch panics. In the recover, we want the stack trace of the code that created the first error in the stack.

Mike Schinkel

unread,
Feb 4, 2025, 7:54:18 PMFeb 4
to Darren Grant, GoLang Nuts Mailing List
On Feb 4, 2025, at 4:59 PM, Darren Grant <darren....@gmail.com> wrote:

What's the current best practice for getting a stack trace from an error passed up from further down in the stack?

E.g. a web server middleware uses recover to catch panics. In the recover, we want the stack trace of the code that created the first error in the stack.

Not sure if I would go so far as saying it is a "best practice," but you can create your own error type — call it TraceError? — and then you can call yourerrpkg.Wrap(err) whenever you want a TraceError and it will return `err` if `err` already contains a TraceError() or it will generate a TraceError() if not.  Or if you want to generate an error from scratch you can call NewTraceError().

Then when you handle the error — such as logging it — you can `errors.As()` to get the TraceError and access its String() or Stack() methods.

Here is a example demoing how it works:


Any follow up questions, feel free to ask.

-Mike

P.S. I threw this together to answer the question, but if anyone else notices something I missed or did wrong, it would be great if you could mention it as I would actually like to use this code moving forward.

Uzondu Enudeme

unread,
Feb 5, 2025, 3:46:04 PMFeb 5
to Mike Schinkel, GoLang Nuts Mailing List

 Hi Mike,

Good stuff and thanks for sharing. 

P.S. I threw this together to answer the question, but if anyone else notices something I missed or did wrong, it would be great if you could mention it as I would actually like to use this code moving forward.

One observation is that the implementation of NewStackError also accepted a variadic parameter for wrapped errors but only used the first one. 
A small update could be to use errors.Join(wrapped...) to capture all passed in errors. 
This is useful when using expressions like `errors.Is`, only the first error is found in the tree even when multiple errors are passed in.
Any exposed API is most likely to be depended upon even when the designer didn't plan for it to be used.

On the naming I also thought TraceError could be called StackError so that NewStackError would return *StackError. 
Maybe we can just rename it to be NewTraceError() since it returns *TraceError. 

Kind regards,

- Uzondu

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/153B81DA-6CD3-4139-A7DE-C482CFF838DD%40newclarity.net.

Mike Schinkel

unread,
Feb 5, 2025, 4:10:39 PMFeb 5
to Uzondu Enudeme, GoLang Nuts Mailing List
Hi Uzondo,

Thanks for the comments.

On Feb 5, 2025, at 3:44 PM, Uzondu Enudeme <will...@gmail.com> wrote:
On the naming I also thought TraceError could be called StackError so that NewStackError would return *StackError. 
Maybe we can just rename it to be NewTraceError() since it returns *TraceError. 

Dammit, that was a refactoring "bug" that I did not catch before posting.  


One observation is that the implementation of NewStackError also accepted a variadic parameter for wrapped errors but only used the first one. A small update could be to use errors.Join(wrapped...) to capture all passed in errors. 
This is useful when using expressions like `errors.Is`, only the first error is found in the tree even when multiple errors are passed in. Any exposed API is most likely to be depended upon even when the designer didn't plan for it to be used.

Interesting idea. Want to tackle that refactor and then post an updated link?

-Mike

Byungjun You

unread,
Feb 6, 2025, 9:29:09 AMFeb 6
to golang-nuts
Hi Mike, thank you so much for sharing your thoughts and all those resources with concrete examples. Those really help me to understand how errors in go work and how it is different from errors in Java.

Based on what you've shared, here's how I understand it:
In essence, because an error in Go is just a value, there’s no built-in way to signal which specific errors might be returned except through documentation. However, since an error is a value, we can also enrich it with additional context like user-friendly messages, stack traces, and more.
Plus, sentinel errors are one way to expose API errors and once we exposed those to API users, it is very important to stick to it and not to change.

And I found the below article very interesting. Please check it out when you have time.

Thank you again for your sharing.

2025년 2월 4일 화요일 오전 3시 2분 37초 UTC+9에 Mike Schinkel님이 작성:

Byungjun You

unread,
Feb 6, 2025, 9:41:29 AMFeb 6
to golang-nuts
Hi Jeremy, I really appreciate all the materials you have shared.

I could understand that wrapping errors with informative messages is one of important best practice treating errors. I'll keep it mind that :)

It was also very helpful to read errors section in Effective Go which you have shared.

Thank you.
2025년 2월 4일 화요일 오전 10시 40분 49초 UTC+9에 Jeremy French님이 작성:

Steven Hartland

unread,
Feb 6, 2025, 11:55:01 AMFeb 6
to Byungjun You, golang-nuts
Watch out for that article is pretty old, predating stdlib wrapping etc.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages