Gleam Panic and Assert

Panic and assert are emergency exits in Gleam. They crash the current process with a clear message when something that should never happen does happen. These tools are not for normal error handling — they are for programming bugs and violated invariants.

panic — Crash With a Message

Call panic when you reach a code path that should be impossible:

pub fn direction_to_degrees(d: Direction) -> Int {
  case d {
    North -> 0
    East  -> 90
    South -> 180
    West  -> 270
  }
}

With exhaustive case, you rarely need panic. But sometimes you hold an invariant that the type system cannot verify:

pub fn head_or_crash(list: List(a)) -> a {
  case list {
    [first, ..] -> first
    []          -> panic as "Expected a non-empty list"
  }
}

panic as "message" Output
──────────────────────────────────────────────────
  → src/main.gleam:8
  panic as "Expected a non-empty list"

The process crashes immediately with this message
and a stack trace.

The panic as Syntax

panic as "This should never happen"
panic as "Corrupted state: " <> context
panic   // without message — less informative

Always include a message. A panic without context is hard to debug — you see a crash but not the reason.

assert — Verify an Assumption

The assert expression verifies a boolean condition. If the condition is False, it panics:

let count = list.length(items)
assert count > 0   // panics if count is 0 or less

assert Flow
──────────────────────────────────────────────────
assert count > 0

count = 5  → condition True  → continues normally
count = 0  → condition False → PANIC

let assert — Pattern Assert

let assert matches a pattern and panics if the match fails:

let assert Ok(port) = int.parse("8080")
// port = 8080
// Panics if int.parse returns Error

let assert vs case
──────────────────────────────────────────────────
let assert — for guaranteed matches (panics on failure):
  let assert Ok(n) = int.parse("42")

case — for handled alternatives (no panic):
  case int.parse(user_input) {
    Ok(n)  -> use_number(n)
    Error(_) -> show_error()
  }

When to Use Each


Decision Guide
──────────────────────────────────────────────────
Situation                         │ Tool
──────────────────────────────────┼────────────────
Handling expected failures        │ Result / case
User input that might be wrong    │ Result / case
Setup code where you control data │ let assert
A case that "cannot happen"       │ panic as "reason"
Verifying a logic invariant       │ assert condition
Tests — known-good input          │ let assert

Panic in Tests

Test frameworks use panics to signal failures:

import gleeunit/should

pub fn add_test() {
  let result = add(2, 3)
  result |> should.equal(5)   // panics if not equal
}

Test panics are caught by the test runner. They appear as test failures, not crashes of the whole test suite.

Process Isolation and Panic

On the BEAM, each Gleam process is isolated. A panic in one process does not crash other processes:


Process Isolation Diagram
──────────────────────────────────────────────────
Process A  → runs normally
Process B  → PANICS
Process C  → runs normally

Process B's crash does not affect A or C.
A supervisor can restart B automatically.

This isolation is why Erlang and Gleam systems can claim high availability. Crashes are contained and recoverable.

Panic Messages Best Practices


Good panic messages:
──────────────────────────────────────────────────
✓ panic as "Payment state machine in invalid state: refunded before paid"
✓ panic as "Config missing required key: database_url"
✓ panic as "List must not be empty before calling max()"

Bad panic messages:
──────────────────────────────────────────────────
✗ panic         → no message — useless for debugging
✗ panic as "error"   → too vague
✗ panic as "bug"     → tells nothing about the bug

Practical Example — State Machine Guard

type OrderState { Draft; Submitted; Paid; Shipped; Cancelled }

pub fn transition(state: OrderState, action: String) -> OrderState {
  case #(state, action) {
    #(Draft, "submit")     -> Submitted
    #(Submitted, "pay")    -> Paid
    #(Paid, "ship")        -> Shipped
    #(Draft, "cancel")     -> Cancelled
    #(Submitted, "cancel") -> Cancelled
    _ ->
      panic as "Invalid transition: " <> action <> " from state " <> debug_state(state)
  }
}

The state machine only allows valid transitions. An invalid combination represents a programming bug — panic is the right response because the caller violated the expected usage contract.

Key Points


Panic and Assert Summary
──────────────────────────────────────────────────
1. panic as "msg"  → crash immediately, for impossible states
2. assert cond     → crash if condition is False
3. let assert pat  → crash if pattern doesn't match
4. Use for programming bugs, never for user errors
5. On BEAM, panics are isolated to the current process
6. Supervisors can restart crashed processes automatically
7. Always write a descriptive message in panic as

Panic and assert are not error handling — they are bug detection. They say "if you reach this code, there is a mistake in the program logic." Used correctly, they make bugs loud, obvious, and easy to locate during development, while production systems handle the rare crash through process supervision.

Leave a Comment

Your email address will not be published. Required fields are marked *