beps/docs/proposals/BEP-001-exceptions/05_deviations_from_ts.md
This document answers common questions from developers familiar with TypeScript/JavaScript exception handling.
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 e matches 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 match expressions. Single expressions don't need braces (e => null), multi-statement handlers use blocks.
See Design Alternatives for the full rationale.
try before every catch?TypeScript requires try:
try {
riskyOperation()
} catch (e) {
handleError(e)
}
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.
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.
TypeScript requires an inner try/catch:
for (const item of items) {
try {
process(item)
} catch (e) {
console.log(`Failed: ${item}`)
}
}
BAML attaches catch to the loop:
for (item in items) {
process(item)
} catch {
e => log(`Failed: ${item}`)
}
Errors are handled per-iteration. Execution continues to the next item.
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:
let result = riskyOperation() catch { e => null }
The result type is the union of the success type and handler return types.
IndexOutOfBounds?TypeScript catches everything:
try {
riskyOperation()
} catch (e) {
// Catches everything, including bugs
}
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.
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.
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:
throw TimeoutError("operation timed out")
throw { code: 500, message: "server error" }
Any value can be thrown. No required base Error type.
| 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) |