In part I of this post, we learned about the error interface and how the standard library provides support for creating error interface values via the errors package. We also learned how to work with error interface values and use them to identify when an error has occured. Finally, we saw how some packages in the standard library export error interface variables to help us identify specific errors.

Knowing when to create and use custom error types in Go can sometimes be confusing. In most cases, the traditional error interface value provided by the errors package is enough for reporting and handling errors. However, sometimes the caller needs extra context in order to make a more informed error handling decision. For me, that is when custom error types make sense.

In this post, we are going to learn about custom error types and look at two use cases from the standard library where they are used. Each use case provides an interesting perspective for when and how to implement a custom error type. Then we will learn how to identify the concrete type of the value or pointer stored within an error interface value, and see how that can help us make more informed error handling decisions.

The net Package

The net package has declared a custom error type called OpError. Pointers of this struct are used by many of the functions and methods inside the package as the concrete type stored within the returned error interface value:

Listing 1.1

01 func Listen(net, laddr string) (Listener, error) {
02  la, err := resolveAddr("listen", net, laddr, noDeadline)
03  if err != nil {
04    return nil, &OpError{Op: "listen", Net: net, Addr: nil, Err: err}
05  }
06  var l Listener
07  switch la := la.toAddr().(type) {
08  case *TCPAddr:
09    l, err = ListenTCP(net, la)
10  case *UnixAddr:
11    l, err = ListenUnix(net, la)
12  default:
13    return nil, &OpError{Op: "listen", Net: net, Addr: la, Err: &AddrError{Err: "unexpected address type", Addr: laddr}}
14  }
15  if err != nil {
16    return nil, err // l is non-nil interface containing nil pointer
17  }
18  return l, nil
19 }

Listing 1.1 shows the implementation of the Listen function from the net package. We can see that on lines 04 and 13, pointers of the OpError struct are created and passed in the return statement for the error interface value. Since pointers of the OpError struct implement the error interface, the pointer can be stored in an error interface value and returned. What you don’t see is that on lines 09 and 11, the ListenTCP and ListenUnix functions can also return pointers of the OpError struct, which are stored in the returned error interface value.

Next, let’s look at the declaration of the OpError struct:

Listing 1.2

01 // OpError is the error type usually returned by functions in the net
02 // package. It describes the operation, network type, and address of
03 // an error.
04 type OpError struct {
05  // Op is the operation which caused the error, such as
06  // "read" or "write".
07  Op string
09  // Net is the network type on which this error occurred,
10  // such as "tcp" or "udp6".
11  Net string
13  // Addr is the network address on which this error occurred.
14  Addr Addr
16  // Err is the error that occurred during the operation.
17  Err error
18 }

Listing 1.2 shows the declaration of the OpError struct. The first three fields on lines 07, 11 and 14 provide context about the network operation being performed when an error occurs. The fourth field on line 17 is declared as an error interface type. This field will contain the actual error that occurred and the value of the concrete type in many cases will be a pointer of type errorString.

Another thing to note is the naming convention for custom error types. It is idiomatic in Go to postfix the name of a custom error type with the word Error. We will see this naming convention used again in other packages.


Error Handling In Go, Part II
1.05 GEEK