Skip to content

Defer, Panic & Recover

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 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
// TypeScript — try/finally for cleanup
async function readFile(path: string): Promise<string> {
const fd = await openFile(path);
try {
return await fd.read();
} finally {
fd.close(); // always runs
}
}
Go
// Go — defer replaces try/finally
func 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
}

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
// TypeScript — explicit ordering in finally
try {
connectDB();
startTransaction();
doWork();
} finally {
rollbackTransaction(); // must be explicit
disconnectDB();
}
Go
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
// TypeScript — throw for any error
function 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
// Go — return error for expected cases; panic for impossible states
func 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 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
// TypeScript — catch wraps throw
function safe(fn: () => void) {
try {
fn();
} catch (err) {
console.error("caught:", err);
}
}
Go
// Go — recover inside a defer
func 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!
}
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)
}
When does a deferred function run?
Multiple defers in the same function run in what order?
Where must recover() be called to stop a panic?
When should you use panic instead of returning an error?