Handle Errors In Go Like A Pro.

Chidozie C. Okafor
6 min readJan 10, 2023

--

Error handling is an important aspect of any software development process, and Go provides a few different ways to handle errors in your code.

In Go, errors are represented by the error interface, which is defined as follows:

type error interface {
Error() string
}

The Error() method returns a string that describes the error. To create an error, you can use the errors.New() function, which takes a string as an argument and returns an error value.

For example, consider a function that divides two integers, and returns an error if the denominator is zero:

package main

import (
"fmt"
"errors"
)

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

func main() {
result, err := divide(5, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}

In the above example, the divide() function returns an error when the denominator is zero. In the main() function, we use the if err != nil idiom to check whether an error occurred, and print an error message if it did.

An alternate way of handling errors is to return multiple values instead of error. For example

package main

import "fmt"

func divide(a, b int) (result int, ok bool) {
if b == 0 {
return
}
result = a / b
ok = true
return
}

func main() {
result, ok := divide(5, 0)
if !ok {
fmt.Println("Error: Division by zero")
} else {
fmt.Println("Result:", result)
}
}

In this case we returned two values from divide function, the result of division and a boolean flag. In this case, the boolean flag indicates whether the operation was successful or not.

Another way is to use ‘defer’ statement, it allows you to run a function after the function it was called in has returned.

package main

import (
"fmt"
"os"
)

func doSomething() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovering from error:", r)
}
}()
// do something that might panic
panic("Something went wrong")
}

func main() {
doSomething()
fmt.Println("This statement will run after panic")
os.Exit(0)
}

In the above example, the recover() function is used to catch a panic and prevent the program from crashing. The defer statement is used to ensure that the recover function runs after the function it was called from has returned. This way, you can use 'defer' statement to handle errors and continue the execution.

It’s important to handle errors properly in your Go code because it helps prevent unexpected behavior and crashes, and makes it easier to debug and fix problems when they occur. Additionally, by handling errors in a consistent and predictable way, you can make your code more robust and reliable.

Another way of handling error is to use custom error types, which allow you to provide more detailed information about an error and also to give you more control over how the error is handled. For example:

package main

import "fmt"

type DivideError struct {
a, b int
msg string
}

func (e *DivideError) Error() string {
return fmt.Sprintf("%d / %d: %s", e.a, e.b, e.msg)
}

func divide(a, b int) (int, error) {
if b == 0 {
return 0, &DivideError{a, b, "division by zero"}
}
return a / b, nil
}

func main() {
result, err := divide(5, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}

Here we created a custom error type DivideError with fields for the input values and an error message. The Error() method returns a string that includes this information. In the divide function, we create an instance of the DivideError type and return it as an error. In the main function we are able to print the custom error message, which would give more detailed information about the error than just a simple string.

One robust approach to error handling in Go is to use a package such as errors which provides additional functionality for working with errors. This package allows you to attach additional context to an error, such as a stack trace or a cause, and also provides a way to wrap existing errors.

For example, consider a function that opens a file, reads its contents, and returns an error if the file could not be opened or read:

package main

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

func readFile(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", errors.Wrap(err, "could not open file")
}
defer file.Close()
data := make([]byte, 100)
count, err := file.Read(data)
if err != nil {
return "", errors.Wrap(err, "could not read file")
}
return string(data[:count]), nil
}

func main() {
result, err := readFile("file.txt")
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}

In the above example, we’re using the errors.Wrap function to attach an additional message to the error returned by the os.Open and file.Read functions. When the error is printed, it includes the original error message as well as the additional message, providing more context about the error.

Error: could not read file: input/output error

Another useful function from this package is errors.Cause, which will traverse the error and its wrapped errors and return the original error that caused the problem. This can be particularly useful when handling errors that may have been wrapped multiple times, it allow you to access the original error and make decision accordingly.

In addition to errors.Wrap and errors.Cause, this package also provides several other functions for creating and working with errors, such as errors.New, errors.Errorf, and errors.WithStack. These functions provide a more robust and powerful way to handle errors in your Go code, making it easier to attach additional context, debug issues, and maintain your application.

It’s worth noting that custom error types can also be built on top of this package and combine its functions with the additional error information you want to show.

Overall, using a package like errors can be a great way to improve the robustness and maintainability of your Go code by providing a more powerful way to handle errors, give more detailed information and make it easier to debug issues when they occur.

For Try-Catch Lovers:

Go does not have a built-in try-catch mechanism like some other programming languages, and Go’s approach to error handling is based on returning error values from functions. However, you can achieve similar behavior by using the defer, panic, and recover functions.

The defer statement allows you to schedule a function to be called after the surrounding function has returned. The panic function causes the current function to stop executing and begins unwinding the call stack. The recover function can be used inside a deferred function to catch panics and return from the surrounding function.

Let’s look at an example that demonstrates how to use the defer, panic, and recover functions to create a try-catch block in Go:

package main

func readFile() (string, error) {
// some code that might cause an error
return "", fmt.Errorf("An error has occurred")
}

func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovering from error:", r)
}
}()

result, err := readFile()
if err != nil {
panic(err)
}
fmt.Println(result)
}

In the above example, the defer statement schedules a function to be called after main has returned. Inside that function, we use recover() to catch any panics that occur in main and return from the function gracefully.

In the main function, we call the readFile function and check for an error. If an error occurs, we use panic(err) to stop the execution of main and start the panic unwinding.

In this way, you can use the defer, panic, and recover functions to create a try-catch block in Go. It's worth noting that this approach should be used carefully, as the use of panic and recover can make the code more complex and harder to debug if not used properly. It's recommended to use them only in specific cases where you want to stop the program execution and be able to handle it in the deferred function.

In general, Go’s error handling approach based on returning error values from functions is a more robust and maintainable way to handle errors, and it is recommended to use it for most cases.

--

--

Chidozie C. Okafor
Chidozie C. Okafor

Written by Chidozie C. Okafor

Software Engineer & Backend Magician 🎩 | Python, Rust | TypeScript, Node.js | Golang | Kafka & GRPC