The most important rule to remember is:
In Go, errors are normal values. A function usually returns an
error, and the caller decides what to do with it.
The basic flow is:
flowchart LR
A[Call function] --> B{Problem?}
B -- No --> C[Return result + nil]
B -- Yes --> D[Return zero value + error]
D --> E{Caller checks err}
E -- err == nil --> F[Continue]
E -- err != nil --> G[Handle error]
For example:
result, err := doSomething()
if err != nil {
// handle the problem
}
fmt.Println(result)
1. Built-in / Standard Error Tools
First, one terminology correction that will make Go easier to understand.
Go has:
| Thing | Type | Purpose |
|---|---|---|
error | Built-in interface | Represents an error |
errors | Standard library package | Creates/checks errors |
fmt | Standard library package | Creates formatted errors |
panic | Built-in function | Stops normal execution |
recover | Built-in function | Catches a panic |
So:
error
panic()
recover()
are built into the language.
But:
import "errors"
import "fmt"
are packages from Go’s standard library.
What?
These are the main tools used for error management.
Why?
Because different situations need different tools:
Simple error → errors.New()
Error containing information → fmt.Errorf()
Check returned error → if err != nil
Unexpected fatal condition → panic()
Catch panic → recover()
When?
Most application code will mainly use:
if err != nil
along with:
errors.New(...)
fmt.Errorf(...)
How / Where?
Here is a complete example showing the main tools.
Complete example
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
// errors.New creates a simple error.
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
// Always check the returned error.
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
Run:
go run .
Output:
Error: cannot divide by zero
Remember
error → type/interface
errors → package
fmt → package
panic → built-in function
recover → built-in function
2. The error Interface
This is the most important concept behind Go errors.
What?
error is a built-in interface.
Conceptually, Go defines it like this:
type error interface {
Error() string
}
That means:
Anything having a method named
Error() stringcan be an error.
Notice the capital E:
Error()
Why?
It allows us to create our own custom error types.
When?
Use custom errors when the error should contain additional information.
For example:
user ID
filename
HTTP status
database information
validation information
How?
Create a type and give it:
Error() string
Where?
Usually inside the package responsible for that particular operation.
Complete example
package main
import "fmt"
// Create our own error type.
type AgeError struct {
Age int
}
// Because AgeError has:
//
// Error() string
//
// it satisfies Go's built-in error interface.
func (e AgeError) Error() string {
return fmt.Sprintf("age %d is too young", e.Age)
}
func checkAge(age int) error {
if age < 18 {
return AgeError{Age: age}
}
return nil
}
func main() {
err := checkAge(15)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Age accepted")
}
Output:
Error: age 15 is too young
The important part is:
func (e AgeError) Error() string
Because of this method:
AgeError
│
│ has Error() string
▼
satisfies
│
▼
error interface
flowchart TD
A[AgeError struct] --> B["Error() string method"]
B --> C[implements error interface]
C --> D[Can be returned as error]
You do not write:
implements error
Go automatically recognizes it.
Remember
type error interface {
Error() string
}
is the heart of Go error management.
3. The errors Package
Now we know that error is an interface.
The next question is:
How do I easily create an error?
One common answer is the errors package.
import "errors"
What?
errors is a standard Go package for creating and working with errors.
The simplest function is:
errors.New()
Example:
errors.New("user not found")
Why?
Because creating a custom struct for every simple error would be unnecessary.
Instead of creating:
type MyError struct {
...
}
you can simply write:
errors.New("something went wrong")
When?
Use errors.New() when the error message is fixed/simple.
Example:
invalid password
user not found
division by zero
file is empty
How?
err := errors.New("something went wrong")
Where?
Usually inside the function where the problem is detected.
Complete example
package main
import (
"errors"
"fmt"
)
func login(password string) error {
if password != "secret123" {
// Create and return a simple error.
return errors.New("invalid password")
}
// nil means there was no error.
return nil
}
func main() {
err := login("wrong-password")
if err != nil {
fmt.Println("Login failed:", err)
return
}
fmt.Println("Login successful")
}
Output:
Login failed: invalid password
The flow is:
login()
│
├── correct password
│ ↓
│ nil
│
└── wrong password
↓
errors.New(...)
↓
error
Most important pattern
return errors.New("something went wrong")
4. errors.New() and fmt.Errorf()
You mentioned Error & Errorf function.
The common functions you will actually use are:
errors.New()
fmt.Errorf()
There isn’t a general built-in Error() function for creating errors.
Error() is normally the method required by the error interface.
errors.New()
Creates a simple fixed error.
errors.New("user not found")
fmt.Errorf()
Creates an error containing dynamic information.
fmt.Errorf("user %s not found", username)
Why Errorf?
Suppose username is:
rajesh
Instead of:
errors.New("user not found")
you might want:
user rajesh not found
That’s where:
fmt.Errorf()
is useful.
When?
Use:
errors.New() → simple/static error
fmt.Errorf() → dynamic/formatted error
Complete example
package main
import (
"errors"
"fmt"
)
func findUser(username string) error {
if username == "" {
// Fixed message:
// errors.New is perfect here.
return errors.New("username cannot be empty")
}
if username != "rajesh" {
// Dynamic message:
// username is inserted into the error.
return fmt.Errorf("user %s not found", username)
}
return nil
}
func main() {
err := findUser("amit")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("User found")
}
Output:
Error: user amit not found
Easy rule
Fixed error:
errors.New("database unavailable")
Dynamic error:
fmt.Errorf("user %s not found", username)
Think:
New = simple message
Errorf = formatted message
5. Error Handling in Go
Now we reach the part you will use constantly.
What?
Error handling means:
- call a function
- receive an error
- check the error
- decide what to do
The famous Go pattern is:
if err != nil {
// handle error
}
Why?
Go intentionally makes error handling explicit.
You can clearly see:
where an error happened
who handled it
what happened after it
When?
Whenever a function returns:
error
check it.
How?
Common pattern:
value, err := someFunction()
if err != nil {
return
}
Where?
Immediately after calling the function.
Prefer:
result, err := divide(10, 2)
if err != nil {
...
}
rather than ignoring err.
Complete example
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
// Success:
// result + nil error
return a / b, nil
}
func main() {
result, err := divide(20, 4)
// Step 1: check error.
if err != nil {
fmt.Println("Failed:", err)
return
}
// Step 2: use result only after checking error.
fmt.Println("Result:", result)
}
Output:
Result: 5
Now change:
divide(20, 4)
to:
divide(20, 0)
Output:
Failed: cannot divide by zero
Understand (result, error)
This is extremely common:
func divide(a, b float64) (float64, error)
Success:
return a / b, nil
means:
Result = valid
Error = nothing
Failure:
return 0, errors.New("cannot divide by zero")
means:
Result = zero value
Error = problem information
Mental model:
flowchart TD
A["divide(20,4)"] --> B{Problem?}
B -- No --> C["return 5, nil"]
B -- Yes --> D["return 0, error"]
C --> E["err == nil"]
D --> F["err != nil"]
E --> G[Use result]
F --> H[Handle error]
The #1 Go error pattern
Memorize this:
result, err := someFunction()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(result)
You will see this everywhere in Go.
6. Errors vs Panics
This distinction is extremely important.
Error
An error means:
Something went wrong, but the program can potentially deal with it.
Examples:
file doesn't exist
wrong password
database unavailable
invalid user input
network timeout
Return:
error
Panic
A panic means:
Normal program execution cannot safely continue.
Example:
panic("something seriously wrong")
A panic starts unwinding the current goroutine’s stack.
Unless recovered, the program terminates and prints a stack trace.
When to use which?
Normal expected problem:
return errors.New("user not found")
Exceptional/programmer/invariant problem:
panic("configuration must never be empty")
Complete example
package main
import (
"errors"
"fmt"
)
// Normal problem:
// return an error.
func checkUsername(username string) error {
if username == "" {
return errors.New("username cannot be empty")
}
return nil
}
// Serious invariant problem:
// panic.
func startApplication(configLoaded bool) {
if !configLoaded {
panic("application configuration was not loaded")
}
fmt.Println("Application started")
}
func main() {
// NORMAL ERROR
err := checkUsername("")
if err != nil {
fmt.Println("Handled error:", err)
}
fmt.Println("Program is still running")
// PANIC
startApplication(false)
// This will never execute.
fmt.Println("Finished")
}
Typical output:
Handled error: username cannot be empty
Program is still running
panic: application configuration was not loaded
Notice the difference.
With error:
if err != nil {
fmt.Println(err)
}
you control what happens.
With:
panic(...)
normal execution stops.
Easy comparison
| Error | Panic |
|---|---|
| Normal problem | Exceptional situation |
| Returned | Thrown by panic() |
| Caller handles it | Stops normal execution |
| Very common | Much less common |
return err | panic(...) |
| Expected failures | Broken assumptions/invariants |
Golden rule
Don’t use panic just because something returned an error.
Usually:
if err != nil {
return err
}
not:
if err != nil {
panic(err)
}
7. Converting Panic to Error
Sometimes you are calling code that may panic, but you don’t want that panic to crash your program.
Go provides:
recover()
But recover() works only from a deferred function during panic unwinding.
The pattern is:
defer func() {
if r := recover(); r != nil {
// panic was caught
}
}()
What?
recover() catches a panic.
Why?
It lets you turn:
panic
into:
ordinary error
When?
Use it mainly at controlled boundaries.
For example:
server request handler
library wrapper
plugin execution
worker/job boundary
code you don't control
Do not use recover() as normal error handling.
How?
This looks strange initially:
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic occurred: %v", r)
}
}()
The defer waits.
If a panic occurs, the deferred function runs and recover() catches the panic.
Complete example
package main
import "fmt"
// This function converts a panic into a normal error.
func safeDivide(a, b int) (result int, err error) {
// This deferred function runs when safeDivide finishes,
// including when a panic happens.
defer func() {
// recover() catches the panic.
if r := recover(); r != nil {
// Convert panic value into a normal error.
err = fmt.Errorf("operation failed: %v", r)
}
}()
if b == 0 {
panic("cannot divide by zero")
}
result = a / b
return result, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
Output:
Error: operation failed: cannot divide by zero
Without recover():
panic
↓
program crashes
With recover():
panic
↓
defer runs
↓
recover()
↓
fmt.Errorf(...)
↓
normal error
↓
caller handles it
flowchart TD
A[Function starts] --> B[defer recovery function]
B --> C[Code runs]
C --> D{Panic?}
D -- No --> E[Normal return]
D -- Yes --> F[Deferred function runs]
F --> G["recover()"]
G --> H["Create error using fmt.Errorf"]
H --> I[Return error to caller]
Important
This:
recover()
does not mean:
catch every error.
It only catches:
panic()
Errors and panics are two different mechanisms.
The Complete Mental Model
You can understand nearly all basic Go error handling with this picture:
flowchart TD
A[Function executes] --> B{Problem occurs?}
B -- No --> C["return result, nil"]
B -- Yes --> D{Normal / expected problem?}
D -- Yes --> E["return result, error"]
E --> F["Caller checks err != nil"]
F --> G[Handle / return / log error]
D -- No --> H["panic(...)"]
H --> I{recover exists?}
I -- No --> J[Program / goroutine terminates]
I -- Yes --> K["recover()"]
K --> L[Possibly convert panic to error]
Final Comparison — Everything Side by Side
| Concept | What is it? | Main purpose | Typical syntax | When to use | Normal? |
|---|---|---|---|---|---|
error | Built-in interface | Represent a problem | var err error | Nearly all recoverable failures | Yes |
Error() string | Interface method | Makes a type satisfy error | func (e MyError) Error() string | Custom errors | Yes |
errors | Standard package | Error utilities | import "errors" | Creating/checking errors | Yes |
errors.New() | Function | Create simple error | errors.New("invalid input") | Static error message | Yes |
fmt.Errorf() | Function | Create formatted error | fmt.Errorf("user %s not found", name) | Dynamic error message | Yes |
if err != nil | Error-handling pattern | Detect returned errors | if err != nil {...} | After error-returning function calls | Extremely common |
nil | No error | Indicates success | return value, nil | Successful operation | Extremely common |
panic() | Built-in function | Stop normal execution | panic("bad state") | Exceptional/unrecoverable situation | Rare |
recover() | Built-in function | Catch panic | recover() | Controlled panic boundary | Rare |
defer + recover | Recovery pattern | Convert/catch panic | defer func(){...}() | Server/library boundaries | Rare |
errors.New vs fmt.Errorf vs panic
This is probably the most useful decision table to remember:
| Situation | Use |
|---|---|
| Simple fixed error | errors.New() |
| Error needs variables/data | fmt.Errorf() |
| Function succeeds | nil |
| Function returns error | if err != nil |
| Normal expected failure | error |
| Truly exceptional impossible state | panic() |
| Need to catch a panic | recover() |
For example:
// Simple
return errors.New("invalid password")
// Dynamic
return fmt.Errorf("user %s not found", username)
// Success
return result, nil
// Handle
if err != nil {
return
}
// Exceptional
panic("database configuration missing")
// Catch panic
if r := recover(); r != nil {
fmt.Println(r)
}
One Very Important Go Pattern to Memorize
If you remember only one piece of code from this tutorial, remember this:
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
The entire philosophy is essentially:
Function
│
├── Success
│ ↓
│ result, nil
│
└── Failure
↓
zero, error
│
▼
Caller checks
│
err != nil
Final Memory Trick
error = WHAT represents a problem
errors.New() = CREATE a simple problem
fmt.Errorf() = CREATE a problem with values/details
err != nil = CHECK whether a problem happened
panic() = STOP because something exceptional happened
recover() = CATCH a panic
defer = WHERE recover is normally placed
And the single most important distinction:
Errors are normal in Go. Panics are exceptional.
For most Go programs, you will spend far more time writing if err != nil than writing panic() or recover().