Categories
Beginner Go tutorial

Sentinel Errors, should we use them in Go?

Sentinel Errors, should we use them in Go?

Sentinel errors, should we use them in Go? When you use a sentinel error in your code, you are providing a way for your consumer to fail gracefully. How can you accomplish this? Let us first understand how to raise an error in Go.


func main() {
  // First way, using errors package
  err := errors.New("You have raise an error using the errors package")
  // Second way, using the fmt package
  uid := "randomUIDValue091"
  err = fmt.Errorf("%s is invalid format.", uid)
}

Currently, you can create an error using two methods. The first as shown in the sample code, you can use the errors package, which in turn enables you to use the New operator to generate an error. The second example shown in the sample code allows you to create an error using the fmt package calling on the Errorf method, which in turn returns an error type.

Sentinel Errors

Now that we understand how to create an error in Go, what is a sentinel error? Some errors are meant to signal that the code can no longer move forward due to the current state. Dave Cheney, a developer who has been active in Go for many years used this term to describe these errors:

The name descends from the practice in computer programming of using a specific value to signify that no further process is possible. So to [sic] with Go, we use specific values to signify an error.

Dave Cheney

Here are some existing examples of a Sentinel Errors are: io.EOF, or constans in the syscall lib syscall.ENOENT.

You typically declare the sentinel error at the package level. You also start the name with Err; io.EOF is a notable exception. Treat these errors as read-only (In go we cannot enforce this, but it would be an error if you as the programmer were to change these). Just to be clear then, when we use sentinel errors this is meant to signal that you cannot start or continue processing.

package main

import (
	"fmt"
	"io"
	"os"
)

const FILE = "/tmp/GeeksforGeeks.txt"

func main() {
	// Create tmp file that is empty
	cFile, err := os.Create(FILE)
	defer cFile.Close()

	// Open the file for read
	rFile, err := os.Open(FILE)
	defer rFile.Close()

	// Read the first 5 bytes
	data := make([]byte, 5)
	_, err = rFile.Read(data)

	if err == io.EOF {
		fmt.Println("You have come to the end of the file")
	}
}

The code above does couple simple tasks, it creates a file, it then opens the file and lastly it reads the 5 first bytes of the file. If the content is empty, which in this case it is, it returns the io.EOF error, meaning there is no more content. In my code, I compare err with the package level sentinel error io.EOF and if true, gracefully deal with it. Follow the link to play around with the above code.

Constant Sentinel error in your code; good idea?

package custerr

// Declare Sentinel type
type Sentinel string

// Implement Error interface 
func(s Sentinel) Error() string {
  return string(s)
}

In the code above, I have declared a type Sentinel, and then implemented the Error interface.

package mypkg

const (
  ErrBz = custerr.Sentinel("bz error")
  ErrBr = custerr.Sentinel("br error")
)

In the code above we are casting a string to type Sentinel; it could be a bit confusing considering it looks like we are calling a function, but that is not the case. The problem with this is, the code is not idiomatic. Using sentinel errors create package coupling which in turn could lead to potential cyclic import errors.

Conclusion

Do not use sentinel errors, we can use other ways to express errors and to check for specific types of errors. Stay tuned, we will address this in a future article! Don’t miss out on other tutorials.