Golang error message

In this article, we'll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so.

In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so.

Error handling in Golang is unconventional when compared to other mainstream languages like Javascript, Java and Python. This can make it very difficult for new programmers to grasp Golangs approach of tackling error handling.

In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so. A basic understanding of Golang is therefore required to follow this article. If you are unsure about any concepts, you can look them up here.

Errors in Golang

Errors indicate an unwanted condition occurring in your application. Let’s say you want to create a temporary directory where you can store some files for your application, but the directory’s creation fails. This is an unwanted condition and is therefore represented using 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)
		}
}

Golang represents errors using the built-in error type, which we will look at closer in the next section. The error is often returned as a second argument of the function, as shown in the example above. Here the TempDir function returns the name of the directory as well as an error variable.

Creating custom errors

As already mentioned errors are represented using the built-in error interface type, which has the following definition:

type error interface {  
    Error() string
}

The interface contains a single method Error() that returns an error message as a string. Every type that implements the error interface can be used as an error. When printing the error using methods like fmt.Println the Error() method is automatically called by Golang.

There are multiple ways of creating custom error messages in Golang, each with its own advantages and disadvantages.

String-based errors

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

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

The errors.New() method can be used to create new errors and takes the error message as its only parameter.

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

fmt.Errorf on the other hand also provides the ability to add formatting to your error message. Above you can see that a parameter can be passed which will be included in the error message.

Custom error with data

You can create your own error type by implementing the Error() function defined in the error interface on your struct. Here is an example:

type PathError struct {
    Path string
}

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

The PathError implements the Error() function and therefore satisfies the error interface. The implementation of the Error() function now returns a string with the path of the PathError struct. You can now use PathError whenever you want to throw an error.

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 can also check if the error has a specific type using either an if or switch statement:

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

This will allow you to extract more information from your errors because you can then call all functions that are implemented on the specific error type. For example, if the PathError had a second method called GetInfo you could call it like this.

e.GetInfo()

Error handling in functions

Now that you know how to create your own custom errors and extract as much information as possible from errors let’s take a look at how you can handle errors in functions.

Most of the time errors are not directly handled in functions but are returned as a return value instead. Here we can take advantage of the fact that Golang supports multiple return values for a function. Thus you can return your error alongside the normal result — errors are always returned as the last argument — of the function 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 is not nil it usually means that there is a problem and you need to handle the error appropriately. This can mean that you use some kind of log message to warn the user, retry the function until it works or close the application entirely depending on the situation. The only drawback is that Golang does not enforce handling the retuned errors, which means that you could just ignore handling errors completely.

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 and therefore provides a way to ignore values in an assignment and avoid compiler errors in the process. But remember that using the blank identifier instead of probably handling errors is dangerous and should not be done if it can be avoided.

Defer, panic and recover

Go does not have exceptions like many other programming languages, including Java and Javascript but has a comparable mechanism know as ,,Defer, panic and recover». Still the use-cases of panic and recover are very different from exceptions in other programming languages as they should only be used in unexpected and unrecoverable situations.

Defer

A defer statement is a mechanism used to defer a function by putting it into an executed stack once the function that contains the defer statement has finished, either normally by executing a return statement or abnormally panicking. Deferred functions will then be executed in reverse order in which they were deferred.

Take the following function for example:

func processHTML(url string) error {
  resp, err := http.Get(url)

  if err != nil {
    	return err
	}

	ct := resp.Header.Get("Content-Type")
	if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
		resp.Body.Close()
		return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
	}

	doc, err := html.Parse(resp.Body)
	resp.Body.Close()

	// ... Process HTML ...
	
	return nil
}

Here you can notice the duplicated resp.Body.Close call, which ensures that the response is properly closed. Once functions grow more complex and have more errors that need to be handled such duplications get more and more problematic to maintain.

Since deferred calls get called once the function has ended, no matter if it succeeded or not it can be used to simplify such calls.

func processHTMLDefer(url string) error {
  resp, err := http.Get(url)

  if err != nil {
    	return err
	}
  defer resp.Body.Close()

	ct := resp.Header.Get("Content-Type")
	if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
		return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
	}

	doc, err := html.Parse(resp.Body)

	// ... Process HTML ...

	return nil
}

All deferred functions are executed in reverse order in which they were deferred when the function finishes.

package main

import (
        "fmt"
)

func main() {
	first()
}

func first() {
	defer fmt.Println("first")
	second()
}

func second() {
	defer fmt.Println("second")
	third()
}

func third() {
	defer fmt.Println("third")
}

Here is the result of running the above program:

third
second
first

Panic

A panic statement signals Golang that your code cannot solve the current problem and it therefore stops the normal execution flow of your code. Once a panic is called, all deferred functions are executed and the program crashes with a log message that includes the panic values (usually an error message) and a stack trace.

As an example Golang will panic when a number is divided by zero.

package main

import "fmt"

func main() {
	divide(5)
}

func divide(x int) {
	fmt.Printf("divide(%d) n", x+0/x)
	divide(x-1)
}

Once the divide function is called using zero, the program will panic, resulting in the following output.

panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.divide(0x0)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2

You can also use the built-in panic function to panic in your own programms. A panic should mostly only be used when something happens that the program didn’t expect and cannot handle.

func getArguments() {
	if len(os.Args) == 1 {
		panic("Not enough arguments!")
	}
}

As already mentioned, deferred functions will be executed before terminating the application, as shown in the following example.

package main

import (
	"fmt"
)

func main() {
	accessSlice([]int{1,2,5,6,7,8}, 0)
}

func accessSlice(slice []int, index int) {
	fmt.Printf("item %d, value %d n", index, slice[index])
	defer fmt.Printf("defer %d n", index)
	accessSlice(slice, index+1)
}

Here is the output of the programm:

item 0, value 1 
item 1, value 2 
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
defer 5
defer 4
defer 3
defer 2
defer 1
defer 0
panic: runtime error: index out of range [6] with length 6

goroutine 1 [running]:
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x6)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:29 +0x250
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x5)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x4)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x3)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x2)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x1)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x0)
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.main()
        C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:9 +0x99
exit status 2

Recover

In some rare cases panics should not terminate the application but be recovered instead. For example, a socket server that encounters an unexpected problem could report the error to the clients and then close all connections rather than leaving the clients wondering what just happened.

Panics can therefore be recovered by calling the built-in recover function within a deferred function in the function that is panicking. Recover will then end the current state of panic and return the panic error value.

package main

import "fmt"

func main(){
	accessSlice([]int{1,2,5,6,7,8}, 0)
}

func accessSlice(slice []int, index int) {
	defer func() {
		if p := recover(); p != nil {
			fmt.Printf("internal error: %v", p)
		}
	}()

	fmt.Printf("item %d, value %d n", index, slice[index])
	defer fmt.Printf("defer %d n", index)
	accessSlice(slice, index+1)
}

As you can see after adding a recover function to the function we coded above the program doesn’t exit anymore when the index is out of bounds by recovers instead.

Output:

item 0, value 1 
item 1, value 2
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
internal error: runtime error: index out of range [6] with length 6defer 5 
defer 4
defer 3
defer 2
defer 1
defer 0

Recovering from panics can be useful in some cases, but as a general rule you should try to avoid recovering from panics.

Error wrapping

Golang also allows errors to wrap other errors which provides the functionality to provide additional context to your error messages. This is often used to provide specific information like where the error originated in your program.

You can create wrapped errors by using the %w flag with the fmt.Errorf function as shown 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 output of the application would now look like the following:

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

As you can see the application prints both the new error created using fmt.Errorf as well as the old error message that was passed to the %w flag. Golang also provides the functionality to get the old error message back 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.

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

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

Casting Errors

Sometimes you will need a way to cast between different error types to for example, access unique information that only that type has. The errors.As function provides an easy and safe way to do so by looking for the first error in the error chain that fits the requirements of the error type. If no match is found the function returns false.

Let’s look at the official errors.As docs example to better understand what is happening.

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)
		}
	}
}

Here we try to cast our generic error type to os.PathError so we can access the Path variable that that specific error contains.

Another useful functionality is checking if an error has a specific type. Golang provides the errors.Is function to do exactly that. Here you provide your error as well as the particular error type you want to check. If the error matches the specific type the function will return true, if not it will return 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.

Sources

  • Golang Blog — Working with Errors in Go 1.13
  • The Go Programming language book
  • Golang Blog — Defer, Panic, and Recover
  • LogRocket — Error handling in Golang
  • GolangByExample — Wrapping and Un-wrapping of error in Go
  • Golang Documentation — Package errors

Conclusion

You made it all the way until the end! I hope this article helped you understand the basics of Go error handling and why it is an essential topic in application/software development.

If you have found this helpful, please consider recommending and sharing it with other fellow developers and subscribing to my newsletter. If you have any questions or feedback, let me know using my contact form or contact me on Twitter.

Introduction

Error handling is quite an important feature of any programming language to improve the quality and transparency between the user and the application. By raising appropriate error messages, the user can get a clear idea about the things happening in the interface as well as the application can handle the errors with appropriate actions.

In the 20th post of the series, we will be exploring the concept of error handling in golang. From this article, we will be able to learn the fundamentals of error or exception handling in golang, create custom error classes, raise and ignore error messages, and exit or redirect the flow of state of the application when an error is raised.

Why we need Error Type

We need error handling and catching in order to stop or divert the flow of the application which will restrict the dubious or unintentional execution of the code. Let’s say, for example, we have a string as an input, and the user skipped the input and the string is returned as empty, we don’t want to execute the further program as the execution might depend on the value of the string. So, in order to catch these kinds of events, we might use errors and log the results for better transparency.

package main

import (
    "fmt"
)

func main() {

    var s string
    n, err := fmt.Scanf("%s", &s)

    if err != nil {
        fmt.Println(err)
        // panic(err)
        // OR
        // return
    } else {
        fmt.Println(n)
        if s[0] == 'a' {
            fmt.Println(s)
        }
    }
}

Enter fullscreen mode

Exit fullscreen mode

$ go run main.go
asdf
1
asdf


$ go run main.go

unexpected newline


$ go run main.go
wsd
1

Enter fullscreen mode

Exit fullscreen mode

In the above example, we have a simple string s input, we will input the string using the Scanf function that will return an integer as the number of variables it has scanned and error if any. Here, as the function might return two values, we need two variables to call the function. The n variable stores the number of variables successfully scanned and the err as the err from the function generated. If there is an error, that is the value stored in err is not empty, we will log the error. And move into the rest of the program.

This might be looking cool, but it doesn’t break out of the program if there is an error. We want it to log the error as well as exit from the program. We can do that using the panic function.

Catching Errors

We’ll see a few examples, where we will catch errors in some of the regularly used functions. These error messages can be used in deciding the next procedure to be run.

Comma OK/Error syntax

We use the comma ok, error syntax where we want multiple return values from a function. It is a narrowed syntax for a two-type return value function, we either return a value that we are expecting as OK, or we get an error from the function call.


ok, err := function()
if err != nil {
    // handle error
    panic(err)
} else {
    // work with the ok object
    fmt.Println(ok)
}

Enter fullscreen mode

Exit fullscreen mode

In the above code, we have used the comma-ok, error syntax, the function call will return two objects either an expected object or an error object if there were errors in the processing. We handle the error if the error object is not empty i.e. it contains something, else we can do the rest of the required processing of the program.

We can even ignore the err or the ok object using the _ i.e. to say a don’t care variable. Remember you can ignore either of the values and not both. It is not recommended to ignore errors but if you know the obvious thing to process, you might as well sometimes.

A more compressed code might look something like below:


if ok, err := function(); err != nil {
    // handle error
    panic(err)
} else {
    // work with the ok object
    fmt.Println(ok)
}

Enter fullscreen mode

Exit fullscreen mode

The above code wraps the initialization of ok, err objects by calling the function inside the if statement and checking for the error.

Making HTTP requests

Let’s say we have a URL, and we want to check if the site exists or not, we can run the http.Get function from the net/http package. We will parse a URL in the function and this function also returns a Response type object and an error object if there are any errors generated during the function call. If there is an error while we call the function, we simply log the error and panic out of the program. Else we can log the status code and do further processing.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    url := "https://meetgor.com/"
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
        panic(err)
    } else {
        fmt.Println(resp.StatusCode)
    }
}

Enter fullscreen mode

Exit fullscreen mode

$ go run web.go
URL: https://meetgor.com/
200



$ go run web.go

URL: htts://meetgor.com/
Get "htts://meetgor.com/": unsupported protocol scheme "htts"
panic: Get "htts://meetgor.com/": unsupported protocol scheme "htts"

goroutine 1 [running]:
main.main()
        /home/meet/code/100-days-of-golang/scripts/errors/https.go:14 +0x170
exit status 2

Enter fullscreen mode

Exit fullscreen mode

This is how we can validate a URL handling the error if the parsed URL is invalid or does not exist.

There is one more variation of the above code style, it is a bit compressed and might be just a syntactic change.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    url := "https://meetgor.com/"
    if resp, err := http.Get(url); err != nil {
        fmt.Println(err)
        panic(err)
    } else {
        fmt.Println(resp.StatusCode)
    }
}

Enter fullscreen mode

Exit fullscreen mode

This can be used wherever you are using the ok, err kind of syntax, but I prefer the clean syntax so I won’t move ahead with this.

Opening or Handling of File

We can even use error handling while dealing with Files or Folders. We can use the os package to read the file in golang. The Open function will read the file if it exists or else it will return an error. We can catch the error from the comma ok,error syntax and do the required processing in the program.

package main

import (
    "fmt"
    "os"
)

func main() {
    file_name := "hello.txt"
    file, err := os.Open(file_name)
    if err != nil {
        fmt.Println("Error: ", err)

        // Create a File 

        // _, err := os.Create(file_name)
        // if err != nil {
        //  fmt.Println(err)
        // }
        // fmt.Println("Created File", file_name)
        // file, _ = os.Open(file_name)
    }
    fmt.Println(file.Stat())
}

Enter fullscreen mode

Exit fullscreen mode

File handling Error

We can use the error as a hint that the file doesn’t exist and create a file and then move toward the actual execution of the program. We can also ignore the file object while creating the file, as we are interested in only knowing that the file is just created without any errors, we use the _ to ignore the variable in the assignment in the function call.

Inside the commented code, we use the Create function to create a file and check for any errors in the process. We finally create the file and Open the newly created file.

Custom Error

We can create custom error types in golang with the help of interfaces and structs. An Error struct will simply consist of a string message, that string will display the error. By overriding or creating the Error method as an interface for the struct we can construct custom errors.

type Invalid_URL_Error struct {
    message string
}

func (e Invalid_URL_Error) Error() string {
    return "Invalid URL"
}

Enter fullscreen mode

Exit fullscreen mode

Here, we have the Invalid_URL_Error as the custom struct name and the Error() method as an interface that will print the error log. This Error method will be used while raising errors in the program. It might be called from another function while doing the actual processing of the URL while sending a GET request.

Further, we can call this custom error method when we wish, by using the package functions, we can override the function call with the custom method.

package main

import (
    "fmt"
    "net/http"
)

type Invalid_URL_Error struct {
    message string
}

func (e Invalid_URL_Error) Error() string {
    return "Invalid URL"
}

func main() {
    url := "htt://meetgor.com"
    response, err := http.Get(url)
    if err != nil {
        fmt.Println(Invalid_URL_Error{})
        fmt.Println(err)
    } else {
        fmt.Println(response)
        defer response.Body.Close()
    }
}

Enter fullscreen mode

Exit fullscreen mode

$ go run custom_error.go
Invalid URL 
Get "htt://meetgor.com": unsupported protocol scheme "htt"

Enter fullscreen mode

Exit fullscreen mode

In the above code, we are basically calling the function http.Get that will return a Response or an err object. We can even call the custom error method with an empty Invalid_URL_Error object, this will call the function Error from that interface. The function will print the custom error message and thereby we are able to log the custom error message for the invalid URL example.

Also, we can parse the default error method to the custom error method which will get us additional information inside the error interface method.

package main

import (
    "fmt"
    "net/http"
)

type Invalid_URL_Error struct {
    message string
}

func (e Invalid_URL_Error) Error() string {
    return "Invalid URL : " + e.message
}

func main() {
    url := "htt://meetgor.com"
    response, err := http.Get(url)
    if err != nil {
        fmt.Println(Invalid_URL_Error{err.Error()})
    } else {
        fmt.Println(response)
        defer response.Body.Close()
    }
}

Enter fullscreen mode

Exit fullscreen mode

$ go run custom_error.go
Invalid URL : Get "htt://meetgor.com": unsupported protocol scheme "htt"

Enter fullscreen mode

Exit fullscreen mode

When the URL is invalid, we will call the custom error interface by parsing the default err.Error method. This will get us the error object from the main method to our custom interface. That is how we will be able to fetch additional information about the error from the interface with the . operator as e.message. So, the syntax is Invalid_URL_Error{err.Error()}, i.e. an object of type Invalid_URL_Error with the message set as the value returned from the default Error() function. hence we can implement the custom error message.

We also need to look for the response object and close the Response Body as it is mandatory to do so and the responsibility of the caller.

Creating a function that returns two values (ok,error)

We can even nest the calling of this error method inside another function. This will give us a good overview of how to deal with errors more thoroughly. We will construct a function with two return values one can be any normal desirable object (which we want from the function call) and the other as an error. This will check for any cases that we can call the custom error and return that error interface and the object which was to be returned will be nil if there is an error. If there are no errors, we will return the object and set the error as nil. This way, we can use the ok, error syntax while calling this function and thereby make it a lot easier in case of complex programs or multiple types of errors.

package main

import (
    "fmt"
    "net/http"
)

type Invalid_URL_Error struct {
    message string
}

func (e Invalid_URL_Error) Error() string {
    return "Invalid URL"
}

func get_resp(url_link string) (http.Response, error) {

    resp, err := http.Get(url_link)

    if err != nil {
        return http.Response{}, &Invalid_URL_Error{}
    } else {
        defer resp.Body.Close()
        return *resp, nil
    }

}

func main() {

    url := "htts://meetgor.com"
    resp, err := get_resp(url)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(resp)
    }

}

Enter fullscreen mode

Exit fullscreen mode

$ go run errors.go
Invalid URL

Enter fullscreen mode

Exit fullscreen mode

This is the simple function get_resp which will either fetch the Response or error. We call the http.Get method internally in the function and if something is invalid, it will return an empty Response but the error will be an Invalid_URL_Error object which is the custom error class. This means, that if we have an error, we will return a string object from the Error method in the interface and if there is no error, we will return the Response object and the error will be set as nil. Hence, you can now realize, why we check for err != nil, it is used for checking if an error object has returned a string or not. As said earlier, we also need to close the request Body.

Further, we can pass the default error method to the custom error class as Invalid_URL_Error{err.Error()}. This will ensure, we get additional context from the custom error interface.

package main

import (
    "fmt"
    "net/http"
)

type Invalid_URL_Error struct {
    message string
}

func (e Invalid_URL_Error) Error() string {
    return "Invalid URL : " + e.message
}

func get_resp(url_link string) (http.Response, error) {

    resp, err := http.Get(url_link)

    if err != nil {
        return http.Response{}, &Invalid_URL_Error{err.Error()}
    } else {
        defer resp.Body.Close()
        return *resp, nil
    }

}

func main() {

    url := "htts://meetgor.com"
    resp, err := get_resp(url)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(resp)
    }

}

Enter fullscreen mode

Exit fullscreen mode

$ go run custom_error
Invalid URL : Get "htts://meetgor.com": unsupported protocol scheme "htts"

Enter fullscreen mode

Exit fullscreen mode

That’s it from this part. Reference for all the code examples and commands can be found in the 100 days of Golang GitHub repository.

Conclusion

From this article, we were able to understand the basics of error handling in golang. We can now work with handling errors in function calls and create custom error interfaces. Thank you for reading, if you have any queries, feedback, or questions, you can freely ask me on my social handles. Happy Coding :)series: «[‘100-days-of-golang’]»

Errors are a language-agnostic part that helps to write code in such a way that no unexpected thing happens. When something occurs which is not supported by any means then an error occurs. Errors help to write clean code that increases the maintainability of the program.

What is an error?

An error is a well developed abstract concept which occurs when an exception happens. That is whenever something unexpected happens an error is thrown. Errors are common in every language which basically means it is a concept in the realm of programming.

Why do we need Error?

Errors are a part of any program. An error tells if something unexpected happens. Errors also help maintain code stability and maintainability. Without errors, the programs we use today will be extremely buggy due to a lack of testing.

Golang has support for errors in a really simple way. Go functions returns errors as a second return value. That is the standard way of implementing and using errors in Go. That means the error can be checked immediately before proceeding to the next steps.

Simple Error Methods

There are multiple methods for creating errors. Here we will discuss the simple ones that can be created without much effort.

1. Using the New function

Golang errors package has a function called New() which can be used to create errors easily. Below it is in action.

package main

import (
	"fmt"
	"errors"
)

func e(v int) (int, error) {
	if v == 0 {
		return 0, errors.New("Zero cannot be used")
	} else {
		return 2*v, nil
	}
}

func main() {
	v, err := e(0)
	
	if err != nil {
		fmt.Println(err, v)      // Zero cannot be used 0
	}	
}

2. Using the Errorf function

The fmt package has an Errorf() method that allows formatted errors as shown below.

fmt.Errorf("Error: Zero not allowed! %v", v)    // Error: Zero not allowed! 0

Checking for an Error

To check for an error we simply get the second value of the function and then check the value with the nil. Since the zero value of an error is nil. So, we check if an error is a nil. If it is then no error has occurred and all other cases the error has occurred.

package main

import (
	"fmt"
	"errors"
)

func e(v int) (int, error) {
	return 42, errors.New("42 is unexpected!")
}

func main() {
	_, err := e(0)
	
	if err != nil {   // check error here
		fmt.Println(err)      // 42 is unexpected!
	}	
}

Panic and recover

Panic occurs when an unexpected wrong thing happens. It stops the function execution. Recover is the opposite of it. It allows us to recover the execution from stopping. Below shown code illustrates the concept.

package main

import (
	"fmt"
)

func f(s string) {
	panic(s)      // throws panic
}

func main() {
        // defer makes the function run at the end
	defer func() {      // recovers panic
		if e := recover(); e != nil {
            		fmt.Println("Recovered from panic")
        	}
	}()
	
	f("Panic occurs!!!") // throws panic 
	
	// output:
	// Recovered from panic
}

Creating custom errors

As we have seen earlier the function errors.New() and fmt.Errorf() both can be used to create new errors. But there is another way we can do that. And that is implementing the error interface.

type CustomError struct {
	data string
}

func (e *CustomError) Error() string {
	return fmt.Sprintf("Error occured due to... %s", e.data)
}

Returning error alongside values

Returning errors are pretty easy in Go. Go supports multiple return values. So we can return any value and error both at the same time and then check the error. Here is a way to do that.

import (
	"fmt"
	"errors"
)

func returnError() (int, error) {  // declare return type here
	return 42, errors.New("Error occured!")  // return it here
}

func main() {
	v, e := returnError()
	if e != nil {
		fmt.Println(e, v)  // Error occured! 42
	}
}

Ignoring errors in Golang

Go has the skip (-) operator which allows skipping returned errors at all. Simply using the skip operator helps here.

package main

import (
	"fmt"
	"errors"
)

func returnError() (int, error) {  // declare return type here
	return 42, errors.New("Error occured!")  // return it here
}

func main() {
	v, _ := returnError()   // skip error with skip operator
	
	fmt.Println(v)    // 42
}

Понравилась статья? Поделить с друзьями:
  • Golang error handling best practice
  • Golang error group
  • Golang error codes
  • Golang error 1040 too many connections
  • Golang defer return error