Skip to Content
Course content

27: Defer, Panic, and Recover

Click on the "Edit" button in the top corner of the screen to edit your slide content.

In most languages, when you open a resource—a file, a database connection, a network socket—you're taught to wrap everything in a try...finally block. It's clunky, it indents your code halfway across the screen, and it separates the "open" logic from the "close" logic by twenty lines of code. In Go, we handle this differently using defer, and when things truly go sideways, we have panic and recover.

The manual cleanup dance

Let's look at how a beginner might handle writing a small configuration file. You open the file, do some work, and close it at the end. It looks clean at first glance:

func saveConfig(filename string, data []byte) error {
    f, err := os.Create(filename)
    if err != nil {
        return err
    }

    _, err = f.Write(data)
    if err != nil {
        // Oh wait, I have to close the file here too!
        f.Close() 
        return err
    }

    return f.Close()
}

The problem here is that as your function grows, the manual cleanup becomes a nightmare. If you have three different error checks, you're calling f.Close() in three different places. I've seen production bugs where a developer added a new if err != nil return and simply forgot to close the resource, leading to a file descriptor leak that crashed the server three days later. It's fragile.

Insurance against early exits

This is where defer comes in. Think of it as a way to schedule a function call to happen the moment the surrounding function returns, regardless of which path it took to get there. I always tell people to put the defer immediately after the resource is successfully acquired.

func saveConfig(filename string, data []byte) error {
    f, err := os.Create(filename)
    if err != nil {
        return err
    }
    // Schedule this now; it will run when saveConfig returns
    defer f.Close()

    _, err = f.Write(data)
    if err != nil {
        return err // f.Close() happens automatically here
    }

    return nil // f.Close() happens automatically here too
}

Now the "open" and "close" logic are right next to each other. You don't have to hunt through the function to make sure every exit path is covered. One thing to keep in mind: if you have multiple defer statements, they execute in Last-In-First-Out (LIFO) order. It's like a stack; the last thing you deferred is the first thing that gets cleaned up.

When errors aren't enough

Now, we need to talk about panic. In Go, we use error for things that are expected to happen (like a file not existing). A panic is for things that should never happen—like accessing an array index that doesn't exist or a nil pointer dereference. It's the "the world is ending" button.

If you're writing a library, you should almost never call panic. It's aggressive. But if you're writing a complex parser or a compiler where a certain state is logically impossible, a panic can be a way to fail fast during development. The catch is that a panic will crash your entire program unless you catch it.

Stopping the bleed with recover

You can stop a panic from crashing your app using recover. However, recover only works inside a deferred function. If you call it in the main flow of your code, it does nothing.

Imagine you're running a web server that processes user-uploaded JSON. One specific edge case in your parser causes a panic. You don't want one bad upload to take down the entire server for every other user. You can wrap the risky logic like this:

func handleRequest(data []byte) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Recovered from a critical error: %v\n", r)
            // Here you'd log the stack trace and send a 500 error to the user
        }
    }()

    // This function might panic if the data is maliciously malformed
    processRiskyData(data)
}

By deferring a function that calls recover, you're essentially creating a safety net. The panic starts climbing up the call stack, hits the deferred function, and is caught. The program stays alive, and you've turned a catastrophic crash into a handled error. Use this sparingly, though. If you use recover to hide bugs instead of fixing them, you're just building a house on a foundation of ghosts.




📋 Practical Task

Exercise: The Robust Log Processor

You are building a system that processes a batch of log files. Some files are corrupted and cause the processing logic to panic. Your goal is to ensure that one corrupted file doesn't stop the entire batch from being processed, and that every file is closed properly regardless of whether it succeeded or crashed.

Requirements:

  • Create a function processFile(name string) that simulates opening a file.
  • Inside processFile, use defer to print "Closing file: [name]".
  • Simulate a crash: if the filename is "corrupt.log", call panic("corrupt file detected!").
  • Create a main loop that iterates through a slice of filenames: []string{"app.log", "corrupt.log", "system.log"}.
  • Wrap the call to processFile in a way that recovers from the panic, prints the error message, and allows the loop to continue to the next file.

Expected Output: The program should process "app.log", hit the panic on "corrupt.log" (but not crash), and successfully process "system.log", with all "Closing file" messages printing for every file attempted.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.