Error Handling in Go: try/catch and Beyond — A Complete Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Error Handling in Go: Philosophy without try/catch

In the world of Go (Golang), there is no try/catch mechanism familiar to many developers. This is not an accident, but a deliberate architectural decision. The language creators believed that explicit error handling makes code more reliable and readable. Instead of hiding exceptions in the call stack, Go offers the programmer to always check the result of every potentially dangerous operation. In this article, we will break down all the main approaches to error handling in Go: from basic error to powerful panic and recover, and also show how to write code that won't crash at the worst possible moment.



1. Basic mechanism: the error interface

The foundation of error handling in Go is the built-in error interface. It has only one method:

type error interface {    Error() string}

Any function that can fail should return error as its last value. It is conventional to return nil if there is no error, and a non-nil value if a failure occurred.

Basic handling example:

package main

import ( "errors" "fmt")

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

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

This is the analogue of try/catch in Go — the explicit check if err != nil. This approach forces the developer to think about errors at every step.



2. Custom error types and wrapping

Often it is not enough to simply return a string with a description. You need to pass context: where the error occurred, what parameters were passed. For this, custom error types are created.

Creating a custom error:

type DivisionError struct {    Dividend float64    Divisor  float64    Message  string}

func (e *DivisionError) Error() string { return fmt.Sprintf("division error: %s (dividend: %f, divisor: %f)", e.Message, e.Dividend, e.Divisor)}

func safeDivide(a, b float64) (float64, error) { if b == 0 { return 0, &DivisionError{Dividend: a, Divisor: b, Message: "divisor is zero"} } return a / b, nil}

Error wrapping:
Starting with Go 1.13, it became possible to wrap one error into another, adding context:

import "fmt"

func readConfig(path string) error { file, err := openFile(path) if err != nil { return fmt.Errorf("failed to read config %s: %w", path, err) } defer file.Close() // ... reading file return nil}

The %w directive creates an error chain. Later, you can check whether an error is a specific type using errors.Is() and errors.As().



3. Panic and Recover: emergency situations

Sometimes something happens that should never happen — accessing an array out of bounds, dereferencing a nil pointer. In such cases, Go uses panic. This is not a replacement for try/catch, but a mechanism for critical, unrecoverable errors.

Panic example:

func main() {    arr := []int{1, 2, 3}    fmt.Println(arr[10]) // panic: runtime error: index out of range}

However, if you want to "catch" a panic and resume execution, recover is used in conjunction with defer. This is the closest analogue to catch.

Recover example:

func safeCall(f func()) {    defer func() {        if r := recover(); r != nil {            fmt.Pr                

Blogs

Book Recommendations