Skip to main content

Command Palette

Search for a command to run...

Day 10 - defer, panic, and recover

Published
9 min read

Error Handling in Go — defer, panic, and recover

In Go, we use defer, panic, and recover statements to handle errors effectively.


🔹 Overview

  • Defer: This statement delays the execution of a function until the surrounding function completes.
  • Panic: Immediately stops the normal flow of the program when an unrecoverable error occurs.
  • Recover: Regains control after a panic to prevent the program from crashing.

Before reading this, check my previous blog — Error Handling in Go.


🧩 Defer in Go

defer schedules a function call to be executed after the surrounding function finishes, no matter how it exits (success, return, or panic).

🧠 Common Uses

  • Closing files, DBs, or network connections
  • Unlocking mutexes
  • Releasing resources

Notes:

  • Defer follows the LIFO (Last In, First Out) model.
  • Deferred functions execute after all other code in the function completes.

🧮 Example — Simple

package main
import "fmt"

func main() {
    // Defer
    defer fmt.Println("one")
    defer fmt.Println("two") // LIFO model: executes this line first, then the above line
    fmt.Println("three")
}

Output:

three
two
one

🧩 Real-Time Scenarios

Scenario 1: Database Connection Cleanup

When you open a database connection or file, you must always close it after use.
If you forget, you may leak resources or lock files.

package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq" // PostgreSQL driver
)

func main() {
    // Open database connection
    db, err := sql.Open("postgres", "user=postgres password=1234 dbname=test sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    // Close connection when function exits
    defer db.Close()

    // Execute a query
    rows, err := db.Query("SELECT id, name FROM users")
    if err != nil {
        log.Fatal(err)
    }
    // Close rows after processing
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        rows.Scan(&id, &name)
        fmt.Println(id, name)
    }
}

Scenario 2: File Handling (Always Close the File)

When reading or writing files, you should always close them once done.
Using defer ensures the file is properly closed, even if an error occurs midway.

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("example.txt")
    if err != nil {
        fmt.Println("Error creating file:", err)
        return
    }
    // Ensure file is closed when function exits
    defer file.Close()

    // Write some content
    _, err = file.WriteString("Hello, Go Developer!")
    if err != nil {
        fmt.Println("Error writing to file:", err)
        return
    }

    fmt.Println("File written successfully!")

    // Even if an error occurs before the function ends,
    // defer ensures the file is closed properly.
}

We also use defer with panic and recover, discussed below.


How Other Languages Handle This

Python

Python uses context managers (with blocks), which automatically call __enter__ at the start and __exit__ when the block ends — similar to Go’s defer.

with open("example.txt", "w") as f:
    f.write("Hello, Python Developer!")

Java — Try-with-Resources

In Java, the try-with-resources statement ensures that any object implementing AutoCloseable is automatically closed at the end of the block.

import java.io.*;

public class Example {
    public static void main(String[] args) {
        try (FileWriter writer = new FileWriter("example.txt")) {
            writer.write("Hello, Java Developer!");
        } catch (IOException e) {
            e.printStackTrace();
        }
        // writer.close() is automatically called
    }
}

JavaScript / TypeScript

JavaScript doesn’t have built-in defer, but you can simulate it using a try...finally block.

function writeFile() {
    const file = openFile("example.txt"); // hypothetical
    try {
        file.write("Hello, JS Developer!");
    } finally {
        file.close(); // runs even if an error occurs
    }
}

💥 Panic in Go

The panic statement is used when something goes terribly wrong — an unrecoverable error occurs that makes it impossible for the program to continue.

🔹 When Panic Happens

  1. The normal flow of execution stops.
  2. All deferred functions are executed (in reverse order).
  3. The program crashes and prints the error message + stack trace.

🧮 Example

package main
import "fmt"

func main() {
    fmt.Println("line1")
    panic("Ending the program")
    fmt.Println("line3") // This line will not execute
}

Real-Time Scenario: Invalid Configuration or Missing .env Variable

If your application requires an environment variable (like a database URL) and it’s missing, you should panic.

package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Println("Starting Application...")

    dbURL := os.Getenv("DB_URL")
    if dbURL == "" {
        panic("❌ Missing required environment variable: DB_URL")
    }

    fmt.Println("Connected to Database:", dbURL)
    fmt.Println("Application running successfully!")
}

If you forget to set DB_URL, the panic statement executes, prints the message, and terminates the program.


🧠 Best Practices for Panic

✅ Do❌ Avoid
Use for truly unrecoverable errorsDon’t use panic for simple validation errors
Log detailed error info before panickingDon’t panic in library code (let the caller handle it)
Always defer cleanup if panic may occurDon’t panic without a clear message

🧩 Recover in Go

recover() is a built-in function that allows a program to regain control after a panic occurs.
It’s used inside a deferred function to stop the panic and prevent the program from crashing.

⚙️ Behavior

  • If called inside a deferred function and a panic is happening → recover() catches it.
  • If called when there’s no panic → it returns nil.

🔹 Example

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

🧩 Real-Time Scenario 1: Recover from Panic to Keep the Server Running

Imagine a web server that must not crash if one request panics.

package main

import "fmt"

func handleRequest(id int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("❌ Recovered from panic in request:", id, "=>", r)
        }
    }()

    fmt.Println("Processing request:", id)

    if id == 3 {
        panic("Something went wrong while processing request!") // simulate crash
    }

    fmt.Println("Request", id, "processed successfully")
}

func main() {
    fmt.Println("Starting server...")

    for i := 1; i <= 5; i++ {
        handleRequest(i)
    }

    fmt.Println("Server still running after handling all requests!")
}

Output:

Starting server...
Processing request: 1
Request 1 processed successfully
Processing request: 2
Request 2 processed successfully
Processing request: 3
❌ Recovered from panic in request: 3 => Something went wrong while processing request!
Processing request: 4
Request 4 processed successfully
Processing request: 5
Request 5 processed successfully
Server still running after handling all requests!

If we remove the recover() function, the program would crash as shown below:

Starting server...
Processing request: 1
Request 1 processed successfully
Processing request: 2
Request 2 processed successfully
Processing request: 3
panic: Something went wrong while processing request!

goroutine 1 [running]:
main.handleRequest(0x3)
        D:/Go Tutorial/programs/Day10/deferPanicRecover.go:15 +0x154
main.main()
        D:/Go Tutorial/programs/Day10/deferPanicRecover.go:25 +0x5b
exit status 2

🧩 Real-Time Scenario 2: Safely Handle Function Errors in a Program

Let’s handle a panic gracefully in a divide function, so the program continues without crashing.

package main

import "fmt"

func safeDivide(a, b int) (result int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
            result = 0 // default safe value
        }
    }()

    if b == 0 {
        panic("Division by zero not allowed!")
    }

    return a / b
}

func main() {
    fmt.Println("Starting calculation...")

    fmt.Println("Result 1:", safeDivide(10, 2))
    fmt.Println("Result 2:", safeDivide(10, 0)) // triggers panic but recovered
    fmt.Println("Result 3:", safeDivide(15, 3))

    fmt.Println("Program completed without crashing")
}

Output:

Starting calculation...
Result 1: 5
Recovered from panic: Division by zero not allowed!
Result 2: 0
Result 3: 5
Program completed without crashing

Real-World Use Cases for Recover

ScenarioExample
Web serversPrevent one crashing request from stopping the entire server
Background jobsRecover from one failed task and continue others
Library codePrevent internal panics from crashing user applications
Graceful shutdownLog and clean up before terminating the program

FAQs — Panic, Recover, and Error Handling in Go

1. How do other languages (Python & Java) handle panic and recover?

In Go, panic and recover work together like a low-level exception system, but Go encourages explicit error handling with error values rather than exceptions.

Other languages, however, rely on exception handling mechanisms like try-catch.

🟢 Python

Python uses the try / except block for exception handling.

try:
    print("Before error")
    raise Exception("Something went wrong!")
except Exception as e:
    print("Recovered:", e)

Java

Java uses try / catch blocks to handle exceptions.

public class Example {
    public static void main(String[] args) {
        try {
            System.out.println("Before error");
            throw new Exception("Something went wrong!");
        } catch (Exception e) {
            System.out.println("Recovered: " + e.getMessage());
        }
    }
}

Key difference:
In Go, panics are not the main way to handle normal errors (unlike exceptions in Python/Java). Go expects you to handle most errors explicitly by returning error values, not by using panic.


2. What is the difference between custom errors and panic in Go?

AspectCustom ErrorsPanic
PurposeHandle expected errors that may occur during normal operationHandle unexpected, unrecoverable conditions
How to createerrors.New("message") or fmt.Errorf("format")panic("message")
Recoverable?Yes — handled by checking if err != nilOnly recoverable via recover()
Example use caseFile not found, invalid input, DB connection failedArray out of bounds, nil pointer dereference, logic error
Control flowProgram continues normally after error handlingNormal execution stops immediately unless recovered

Example — Custom Error:

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

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

3. When should you use panic and recover?

SituationShould you use panic?
Recoverable issues (like invalid user input, failed API calls)No — use custom error
Unrecoverable errors (like corrupted state, missing config)Yes, panic is justified
Library code returning error to callerNever panic, always return error
Top-level code (like main or goroutine handlers)Can use recover() to prevent crash

Rule of thumb:

Use errors for expected problems.
Use panic/recover for truly exceptional situations.


4. Is try-catch the same as panic-recover?

Not exactly.

In Java/Python, exceptions are integrated into the language — all errors are raised and caught using try-catch.

In Go, panics are rarely used, and most code uses explicit error values to keep control flow simple and predictable.

ConceptGoPython / Java
Common Error HandlingReturn error valuesRaise exceptions
Critical FailuresUse panicThrow exception
Recovery Mechanismrecover() inside defertry-catch block

More from this blog

G

GoSprint90

16 posts

Welcome to GoQuest90 — a 90-day journey from absolute beginner to confident Go developer.