Defer, Panic & Recover
JavaScript: try / finally / throw
Section titled “JavaScript: try / finally / throw”TypeScript handles cleanup and errors with constructs you know well: try/finally to run cleanup code regardless of errors, and throw/catch for error propagation.
Go has analogues to all three, but they look different and have important behavioral differences.
defer — cleanup that always runs
Section titled “defer — cleanup that always runs”defer schedules a function call to run when the surrounding function returns — no matter how it returns (normally, via return, or after a panic). This replaces the common try/finally cleanup pattern.
// TypeScript — try/finally for cleanupasync function readFile(path: string): Promise<string> { const fd = await openFile(path); try { return await fd.read(); } finally { fd.close(); // always runs }}// Go — defer replaces try/finallyfunc readFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() // will run when readFile returns
data, err := io.ReadAll(f) if err != nil { return "", err } return string(data), nil}defer executes in LIFO order
Section titled “defer executes in LIFO order”Multiple deferred calls stack up and execute in last-in, first-out order — the reverse of how they were registered. This mirrors how you usually want to undo setup steps.
// TypeScript — explicit ordering in finallytry { connectDB(); startTransaction(); doWork();} finally { rollbackTransaction(); // must be explicit disconnectDB();}func doWork() { connectDB() defer disconnectDB() // registered 1st, runs 2nd
startTransaction() defer rollbackTransaction() // registered 2nd, runs 1st
// if any step panics, both defers still run (LIFO) processRows()}panic — like throw, but for unrecoverable errors
Section titled “panic — like throw, but for unrecoverable errors”panic stops the current goroutine, unwinds the stack (running all deferred functions along the way), and crashes the program if nothing intercepts it. Use it only for programming errors that should never happen in correct code — not for expected errors you want to propagate to callers.
// TypeScript — throw for any errorfunction divide(a: number, b: number): number { if (b === 0) throw new Error("division by zero"); return a / b;}try { divide(10, 0);} catch (e) { console.error(e.message);}// Go — return error for expected cases; panic for impossible statesfunc divide(a, b float64) float64 { if b == 0 { panic("divide by zero — this is a programming error") } return a / b}
// For expected errors, return (T, error) instead:func safeDivide(a, b float64) (float64, error) { if b == 0 { return 0, fmt.Errorf("cannot divide %v by zero", a) } return a / b, nil}recover — catch a panic
Section titled “recover — catch a panic”recover stops a panic from propagating. It must be called inside a deferred function to work. This is how Go libraries (and HTTP servers) prevent one bad request from crashing the whole program.
// TypeScript — catch wraps throwfunction safe(fn: () => void) { try { fn(); } catch (err) { console.error("caught:", err); }}// Go — recover inside a deferfunc safeRun(fn func()) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic recovered: %v", r) } }() fn() return nil}
func main() { err := safeRun(func() { panic("oh no!") }) fmt.Println(err) // panic recovered: oh no!}Try it
Section titled “Try it”package main
import "fmt"
func divide(a, b float64) (result float64, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered from panic: %v", r) } }() if b == 0 { panic("division by zero") } return a / b, nil}
func withCleanup() { fmt.Println("1. start") defer fmt.Println("3. cleanup (deferred)") fmt.Println("2. work")}
func multiDefer() { for i := 1; i <= 3; i++ { defer fmt.Printf("deferred %d\n", i) // LIFO: 3, 2, 1 }}
func main() { withCleanup() fmt.Println("---")
multiDefer() fmt.Println("---")
r1, err := divide(10, 2) fmt.Printf("10/2 = %.1f err=%v\n", r1, err)
r2, err := divide(5, 0) fmt.Printf("5/0 = %.1f err=%v\n", r2, err)}Loading Go runtime (first run only, ~8 MB)…