Safe Functions: Practical Guide
This document covers the safe keyword for strict error handling guarantees.
Using safe
How do I guarantee a function handles all its errors?
Add the safe keyword to the function declaration:
safe function Extract(text: string) -> Resume | null {
client "gpt-4o"
prompt #"..."#
} catch {
e: TimeoutError => null
e: RefusalError => null
// Compiler enforces: all Error types must be handled
}
Without safe, unhandled errors implicitly propagate. With safe, the compiler requires exhaustive handling—if you miss an error type, it fails to compile.
How do I enforce exhaustive error handling at a call site?
Add safe before the expression:
Or apply it to a block:
The compiler ensures the attached catch block handles all possible errors from the expression or block.
What does safe guarantee?
safe guarantees that no Error types escape the scope. It does not catch Panic types.
With safe |
Without safe |
|---|---|
| All Errors must be handled | Unhandled Errors propagate |
| Compiler enforces exhaustiveness | Compiler allows partial handling |
| Panics still propagate | Panics still propagate |
A safe function can still crash if a bug occurs (e.g., assert() fails, array access panics).
Does safe catch Panics?
No. safe only applies to Error types. Panics propagate through safe functions just like any other function.
safe function GetFirst(items: Item[]) -> Item | null {
return items[0] // Can panic with IndexOutOfBounds
} catch {
e => null // Catches Errors, not Panics
}
If items is empty, IndexOutOfBounds crashes the program despite the safe keyword.
What happens if I add a new error type to a safe function?
The compiler fails immediately:
safe function Extract(text: string) -> Resume | null {
client "gpt-4o"
prompt #"..."#
} catch {
e: TimeoutError => null
// Error: NetworkError is not handled
}
Without safe, the new error would silently propagate up the stack. With safe, you're forced to handle it.
Can the compiler infer if a function is safe?
Yes. If a function handles all its errors, the compiler infers it as "semantically safe" even without the keyword.
However, adding safe explicitly makes this a checked contract. If someone later changes the implementation to introduce an unhandled error, the compiler catches it.
// Implicitly safe (compiler infers)
function Extract(text: string) -> Resume | null {
client "gpt-4o"
prompt #"..."#
} catch {
e => null // Handles everything
}
// Explicitly safe (compiler enforces)
safe function Extract(text: string) -> Resume | null {
client "gpt-4o"
prompt #"..."#
} catch {
e => null
}
Tooling
Visual Safety Indicators
IDEs display warnings when a function contains unhandled unsafe calls:
1 │ function ProcessBatch(texts: string[]) -> Report {
2 │ let results = []
3 │ for (text in texts) {
4 │ ⚠ let resume = Extract(text) // unsafe: can throw LLMError
5 │ results.append(resume)
6 │ }
7 │ ⚠ let summary = Summarize(results) // unsafe: can throw LLMError
8 │ return Report { results, summary }
9 │ }
Hovering shows which errors can propagate:
⚠ Extract(text) can throw: LLMError, TimeoutError, ParseError
Add error handling:
Extract(text) catch { e => defaultResume }
Inline Diagnostics
The compiler flags unsafe calls inside safe functions:
safe function Process() -> Result {
let x = Extract(text)
// ~~~~~~~~~~~~~ error: unsafe call in safe function
// Extract() can throw LLMError, TimeoutError
// hint: add `catch { ... }` or use `safe Extract(text) catch { ... }`
let y = Extract(text) catch { e => default }
// OK: error handling present
}
Agent Metadata
Generated client code includes safety metadata:
// baml_client/metadata.ts
export const BAML_FUNCTION_METADATA = {
Extract: {
isSafe: false,
canThrow: ["LLMError", "TimeoutError", "ParseError"],
},
SafeExtract: {
isSafe: true,
canThrow: [],
},
FormatName: {
isSafe: true,
canThrow: [],
}
}
Agents can query this before generating code:
if (BAML_FUNCTION_METADATA.Extract.isSafe) {
return `Extract(text)`
} else {
return `Extract(text) catch { e => null }`
}
CLI Safety Analysis
$ baml safety graph --function ProcessBatch
ProcessBatch (unsafe)
├─⚠ Extract (unsafe)
│ └─ client "openai/gpt-4o"
├─⚠ Summarize (unsafe)
│ └─ client "openai/gpt-4o"
└─● FormatReport (safe)
Legend:
● = safe ⚠ = unsafe [caught] = error handling present
With error handling added: