Coming from TypeScript/JavaScript
This document answers common questions from developers familiar with TypeScript/JavaScript exception handling.
Why does catch use pattern matching instead of catch (e) { ... }?
TypeScript binds one variable, then uses instanceof to discriminate:
try {
riskyOperation()
} catch (e) {
if (e instanceof TimeoutError) {
retry()
} else if (e instanceof ParseError) {
return null
} else {
throw e
}
}
BAML uses pattern matching with arrow syntax:
riskyOperation() catch {
e: TimeoutError => retry()
e: ParseError => null
// Other errors implicitly propagate
}
Why not catch (e) { <pattern matching> }?
We could have kept the header binding and added pattern matching inside:
// Hypothetical: header binding + pattern matching
catch (e) {
e: TimeoutError => retry()
e: ParseError => null
}
But this creates redundancy: you bind e in the header, then re-bind it in each pattern arm. Which e is in scope? The outer untyped one or the inner typed one?
By removing the header binding, the design is cleaner: each pattern arm introduces its own binding with the matched type already applied.
Other benefits:
-
Untyped patterns exclude Panics: A pattern like
ematches all Errors but not Panics. Bugs crash loudly by default. To catch panics explicitly:p: Panic => .... -
Implicit re-throw: Unhandled cases propagate automatically. No
else { throw e }boilerplate. -
Arrow syntax: Consistent with
matchexpressions. Single expressions don't need braces (e => null), multi-statement handlers use blocks.
See Design Alternatives for the full rationale.
Do I need to write try before every catch?
TypeScript requires try:
BAML allows catch on any expression, so try is optional:
// Catch on a function call
a() catch { e => null }
// Catch on a binary expression
a() + b() catch { e => 0 }
// Catch on a block expression
{ let x = a(); x + b() } catch { e => 0 }
// Catch on a block with explicit try (for familiarity)
try { let x = a(); x + b() } catch { e => 0 }
Since catch attaches to any expression—including block expressions—the try keyword is redundant. We allow try as a prefix for familiarity, but it adds no semantic meaning.
How do I add error handling to a function without wrapping the body?
TypeScript requires wrapping the function body:
function extract(text: string): Resume | null {
try {
return callLLM(text)
} catch (e) {
return null
}
}
BAML attaches catch directly to the function:
function Extract(text: string) -> Resume | null {
client "gpt-4o"
prompt #"Extract resume from {{ text }}"#
} catch {
e => null
}
No restructuring needed. Particularly useful for declarative LLM functions.
How do I handle errors in a loop without nesting try/catch?
TypeScript requires an inner try/catch:
BAML attaches catch to the loop:
Errors are handled per-iteration. Execution continues to the next item.
Why is catch an expression instead of a statement?
TypeScript try/catch is a statement, requiring variable hoisting or an IIFE:
// Hoisting required
let result
try {
result = riskyOperation()
} catch (e) {
result = null
}
// Or use an IIFE
const result2 = (() => {
try { return riskyOperation() }
catch (e) { return null }
})()
BAML catch is an expression—no hoisting or wrapping needed:
The result type is the union of the success type and handler return types.
Why doesn't my catch-all pattern catch IndexOutOfBounds?
TypeScript catches everything:
BAML distinguishes errors from panics:
riskyOperation() catch {
e => null // Catches recoverable errors only
// IndexOutOfBounds, AssertionError, etc. propagate
}
// To catch panics explicitly:
riskyOperation() catch {
p: Panic => handleBug(p)
e => null
}
Untyped patterns like e match recoverable errors but not Panic types. Bugs fail loudly by default.
Is there a finally block?
TypeScript supports finally:
let handle
try {
handle = acquireResource()
useResource(handle)
} catch (e) {
logError(e)
} finally {
if (handle) releaseResource(handle)
}
BAML handles cleanup through normal control flow:
let handle = acquireResource()
let result = {
useResource(handle)
} catch {
e => {
logError(e)
null
}
}
releaseResource(handle)
No finally keyword. Place cleanup after the catch expression.
What types can I throw?
TypeScript allows any value but convention is Error:
throw "string error" // Valid but discouraged
throw new Error("msg") // Idiomatic
throw { code: 500 } // Valid but discouraged
BAML uses an open throw system:
Any value can be thrown. No required base Error type.
Summary Table
| Question | TypeScript | BAML |
|---|---|---|
| How do I handle errors by type? | catch (e) { if (e instanceof ...) } |
Pattern matching: catch { e: Type => ... } |
| Can I use catch as an expression? | No (needs IIFE) | Yes |
Is try required? |
Yes | No (optional) |
| Can I attach catch to functions? | No | Yes |
| Can I attach catch to loops? | No | Yes |
Is there a finally? |
Yes | No |
| Does catch-all catch everything? | Yes | No (excludes Panic) |
| What can I throw? | Any (convention: Error) | Any (no convention) |