go-tutorial

Go Error Handling

When compared to other mainstream languages like Javascript, Java, and Python, Golang’s error handling is unique. This can make understanding Golang’s approach to error handling difficult for novice programmers.

Errors in the Golang programming language

Errors signal that something is wrong with your application. Let’s imagine you want to establish a temporary directory to store some files for your programme, but you can’t get it to work. Because this is an undesirable circumstance, it is represented with an error.

package main

import (  
    "fmt"
    "ioutil"
)

func main() {  
    dir, err := ioutil.TempDir("", "temp")
		if err != nil {
			return fmt.Errorf("failed to create temp dir: %v", err)
		}
}

The built-in error type in Golang is used to represent errors, which we’ll go over in more detail in the next section. As illustrated in the example above, the error is frequently returned as the function’s second argument. The TempDir function returns the directory name as well as an error variable in this case.

Making your own errors

As previously stated, errors are represented by the built-in error interface type, which is defined as follows:

type error interface {  
    Error() string
}

Error() is a single method in the interface that returns an error message as a string. An error can be any type that implements the error interface. When using techniques like fmt to print the error. Golang calls the Error() function of Println automatically.

In Golang, there are several methods for constructing bespoke error messages, each with its own set of benefits and drawbacks.

Errors caused by strings

String-based errors are used for simple faults that only need to return an error message and can be created using two out-of-the-box Golang options.

err := errors.New("math: divided by zero")

The flaws. The error message is the single parameter of the New() method, which can be used to create new errors.

err2 := fmt.Errorf("math: %g cannot be divided by zero", x)

The fmt.Errorf class, on the other hand, allows you to format your error message. As can be seen in the example above, a parameter can be given that will be included in the error message.

Data-specific mistake

By implementing the Error() function defined in the error interface on your struct, you can construct your own error type. Here’s an illustration:

type PathError struct {
    Path string
}

func (e *PathError) Error() string {
	return fmt.Sprintf("error in path: %v", e.Path)
}

PathError implements the Error() method and hence meets the error interface requirements. The Error() function now returns a string that contains the path of the PathError object. You may now use PathError to throw an error whenever you wish.

Here is an elementary example:

package main

import(
	"fmt"
)

type PathError struct {
    Path string
}

func (e *PathError) Error() string {
	return fmt.Sprintf("error in path: %v", e.Path)
}

func throwError() error {
	return &PathError{Path: "/test"}
}

func main() {
	err := throwError()

	if err != nil {
		fmt.Println(err)
	}
}

You may also use an if or switch statement to see if the error is of a specified type:

if err != nil {
    switch e := err.(type) {
    case *PathError :
        // Do something with the path
    default:
        log.Println(e)
    }
}

Because you may then call all functions that are implemented on the specified error type, you will be able to extract additional information from your problems. If the PathError had a second method called GetInfo, for example, you could call it like this.

e.GetInfo()

Handling errors in functions

Let’s look at how to handle errors in functions now that you know how to construct your own custom errors and extract as much information as possible from them.

Errors are usually not directly handled in functions, but instead returned as a return value. We can use the fact that Golang allows multiple return values for a function in this case. As a result, you can return your error alongside the function’s normal result (errors are always returned like the last argument) as follows:

func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0.0, errors.New("cannot divide through zero")
	}

	return a/b, nil
}

The function call will then look similar to this:

func main() {
	num, err := divide(100, 0)

	if err != nil {
		fmt.Printf("error: %s", err.Error())
	} else {
		fmt.Println("Number: ", num)
	}
}

If the returned error isn’t nil, there’s likely an issue, and you’ll need to deal with it accordingly. Depending on the situation, this could involve using a log message to notify the user, retrying the function until it works, or closing the programme entirely. The main disadvantage is that Golang does not require you to handle returned errors, which means you might simply disregard them.

Take the following code for example:

package main

import (
	"errors"
	"fmt"
)

func main() {
	num2, _ := divide(100, 0)
	
	fmt.Println("Number: ", num2)
}

The so-called blank identifier is used as an anonymous placeholder, allowing you to ignore values in an assignment while also avoiding compiler issues. However, keep in mind that utilising the blank identifier instead of handling errors is risky and should be avoided if possible.

Error Wrapping

Errors in Golang can also surround other errors, giving you the ability to provide more context to your error messages. This is frequently used to convey detailed information, such as the location of the problem in your software.

The percent w flag with the fmt.Errorf function can be used to construct wrapped errors, as seen in the following example.

package main

import (
	"errors"
	"fmt"
	"os"
)

func main() {
	err := openFile("non-existing")

	if err != nil {
		fmt.Printf("error running program: %s \n", err.Error())
	}
}

func openFile(filename string) error {
	if _, err := os.Open(filename); err != nil {
		return fmt.Errorf("error opening %s: %w", filename, err)
	}

	return nil
}

The application’s output would now look something like this:

error running program: error opening non-existing: open non-existing: no such file or directory

The application prints both the new error created with fmt.Errorf and the old error message supplied to the percent w flag, as you can see. Golang also has the ability to revert to an earlier error message by unwrapping the error using errors. Unwrap.

package main

import (
	"errors"
	"fmt"
	"os"
)

func main() {
	err := openFile("non-existing")

	if err != nil {
		fmt.Printf("error running program: %s \n", err.Error())

		// Unwrap error
		unwrappedErr := errors.Unwrap(err)
		fmt.Printf("unwrapped error: %v \n", unwrappedErr)
	}
}

func openFile(filename string) error {
	if _, err := os.Open(filename); err != nil {
		return fmt.Errorf("error opening %s: %w", filename, err)
	}

	return nil
}

As you can see the output now also displays the original error.

Errors can be wrapped and unwrapped multiple times, but in most cases wrapping them more than a few times does not make sense.

Casting Error

You may occasionally require a means to cast between multiple error kinds, for example, to gain access to information that is unique to that type. The flaws. The as function makes this simple and safe by looking for the first error in the error chain that matches the error type’s requirements. The function returns false if no match is found.

Let’s have a look at the official mistakes.

To further understand what is going on, use the example of documents.

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

func main(){
	// Casting error
	if _, err := os.Open("non-existing"); err != nil {
		var pathError *os.PathError
		if errors.As(err, &pathError) {
			fmt.Println("Failed at path:", pathError.Path)
		} else {
			fmt.Println(err)
		}
	}
}

We’re attempting to cast our general error type to os.PathError in order to gain access to the Path variable contained in that specific issue.

Checking if an error is of a specified type is another valuable feature. The errors are provided by Golang. Is a function that allows you to do just that. Here you specify your error as well as the sort of error you want to investigate. If the error is of the correct kind, the method returns true; otherwise, it returns false.

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

func main(){
	// Check if error is a specific type
	if _, err := os.Open("non-existing"); err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			fmt.Println("file does not exist")
		} else {
			fmt.Println(err)
		}
	}
}


After checking, you can adapt your error message accordingly.

RECOMMENDED ARTICLES





Leave a Reply

Your email address will not be published. Required fields are marked *