recover-go-recuperar-panic

Recover in Go: capturing panic with defer without overusing it

  • 3 min

recover in Go is a special function that allows catching a panic inside a defer and regaining control of the goroutine.

In the previous article, we saw that panic is not the normal mechanism for handling errors. recover does not turn Go into a try-catch language either. It’s more like a safety net for very specific program boundaries.

If something panics, we can prevent the entire process from terminating, log the problem, and respond in a controlled manner. Recovery must happen in the same goroutine.

recover only works inside defer

recover catches a panic when the deferred function that is executing calls it directly.

func main() {
    defer func() {
        err := recover()
        if err != nil {
            fmt.Println("Recovered:", err)
        }
    }()

    panic("something went very wrong")
}
Copied!

Output:

Recovered: something went very wrong
Copied!

If we call recover() outside a defer, it simply returns nil. It does nothing special.

What recover returns

panic can receive any value:

panic("message")
panic(123)
panic(errors.New("critical error"))
Copied!

That’s why recover returns any.

defer func() {
    err := recover()
    if err != nil {
        fmt.Printf("type: %T, value: %v\n", err, err)
    }
}()
Copied!

In real code, it’s common to convert that value into a log message or an internal error.

Typical pattern in servers

A common use case for recover is to protect each HTTP request.

func recoverMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v", err)
                http.Error(w, "Internal error", http.StatusInternalServerError)
            }
        }()

        next.ServeHTTP(w, r)
    })
}
Copied!

This way, if a handler has a bug, the middleware can attempt to return a 500 error and the server will continue handling other requests. If the response has already sent its headers, we can no longer change its status code.

The net/http server already recovers from panics in ServeHTTP, logs a trace, and closes the connection or resets the HTTP/2 stream. A custom middleware is still useful if we want to control the response and logging.

This does not mean the bug is fixed. We’ve only prevented one request from crashing the entire process. The error must be logged, and the root cause must be corrected.

Don’t use recover as try-catch

This is a bad idea:

func leerConfig() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("something failed")
        }
    }()

    // Normal business logic
}
Copied!

If a function can fail in an expected way, it should return error.

func leerConfig(ruta string) ([]byte, error) {
    datos, err := os.ReadFile(ruta)
    if err != nil {
        return nil, err
    }

    return datos, nil
}
Copied!

Expected errors go with error. panic is for exceptional situations, bugs, or unrecoverable states.

recover and goroutines

An important detail: recover only catches panics inside the same goroutine.

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("recovered")
        }
    }()

    go func() {
        panic("failure in another goroutine")
    }()

    time.Sleep(time.Second)
}
Copied!

That recover in main does not catch the panic from the goroutine. If you want to protect a goroutine, the defer must be inside that goroutine.

go func() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic in worker:", err)
        }
    }()

    trabajar()
}()
Copied!

When it makes sense

recover usually makes sense at system boundaries:

  • HTTP servers, to isolate a broken request.
  • Workers, to log a failure without losing the entire process.
  • Libraries that execute user callbacks.

Outside of these cases, returning error is typically better.