Source: src/hql/transpiler/pipeline/transform/try-catch.ts
try-expr ::= '(' 'try' body... catch-clause? finally-clause? ')'
catch-clause ::= '(' 'catch' param? body... ')'
finally-clause ::= '(' 'finally' body... ')'
throw-expr ::= '(' 'throw' expr ')'
HQL provides structured error handling via try/catch/finally/throw. All error handling forms are expressions that return values. This is achieved through auto-IIFE wrapping of try blocks.
;; try-only (no catch or finally)
(try
(risky-operation))
;; try + catch with named parameter
(try
(risky-operation)
(catch e
(handle-error e)))
;; try + catch without parameter
(try
(risky-operation)
(catch
(fallback-value)))
;; try + finally
(try
(open-resource)
(finally
(close-resource)))
;; try + catch + finally
(try
(open-resource)
(do-work)
(catch e
(log-error e))
(finally
(close-resource)))
All try expressions are wrapped in an IIFE (Immediately Invoked Function Expression) so they can be used as expressions that return values:
(let result (try
(parse-json input)
(catch e "default")))
Compiles to:
const result = (() => {
try {
return parseJson(input);
} catch (e) {
return "default";
}
})();
The catch clause binds the caught error to a named parameter:
(catch e (handle e)) ;; named parameter
(catch (fallback)) ;; parameterless
catch, it becomes the catch parameter identifiercatch clause is allowed per try block (multiple catch clauses raise a ValidationError)The finally clause executes cleanup code regardless of whether an error occurred:
(finally (cleanup))
finally clause is allowed per try blockThe try body can contain multiple expressions. All expressions before catch/finally are part of the try body:
(try
(step-one)
(step-two)
(step-three) ;; last expression is the return value
(catch e
(handle e)))
When the try block, catch body, or finally body contains await expressions, the IIFE wrapper is automatically made async and the call is wrapped in await:
(try
(await (fetch-data url))
(catch e
(await (log-error e))))
Compiles to:
await (async () => {
try {
return await __hql_consume_async_iter(fetchData(url));
} catch (e) {
return await __hql_consume_async_iter(logError(e));
}
})();
When the try block, catch body, or finally body contains yield expressions, the IIFE wrapper is made a generator and the call is wrapped in yield*:
(fn* producer [items]
(try
(for-of [item items]
(yield item))
(catch e
(yield "error"))))
Compiles to:
function* producer(items) {
yield* (function* () {
try {
for (const item of items) {
yield item;
}
} catch (e) {
yield "error";
}
})();
}
The throw form throws an error:
(throw (new Error "Something went wrong"))
(throw "string error")
(throw e) ;; rethrow caught error
Note: throw is handled by src/hql/transpiler/syntax/conditional.ts (alongside return), not by try-catch.ts.
| Rule | Error |
|---|---|
try with no body | "try requires a body" |
try with empty body (all clauses, no expressions) | "try requires at least one body expression" |
Multiple catch clauses | "Multiple catch clauses are not supported" |
Multiple finally clauses | "Multiple finally clauses are not supported" |
Empty catch body | "catch requires a body" |
Empty finally body | "finally requires a body" |
Unknown clause (not catch/finally) | "Unknown clause 'X' in try statement" |
try always returns a value via IIFE wrappingawait in any sub-block makes the IIFE asyncyield in any sub-block makes the IIFE a generatorcatch/finally clausestry list nodeValid but uncommon. The try body is still IIFE-wrapped for expression semantics:
(let result (try (compute-value)))
Each try gets its own IIFE wrapper:
(try
(try
(inner-operation)
(catch e1 (handle-inner e1)))
(catch e2 (handle-outer e2)))
Recursive calls inside try/catch/finally are NOT in tail position (per JavaScript semantics) and will not be optimized by TCO.
src/hql/transpiler/pipeline/transform/try-catch.tssrc/hql/transpiler/syntax/conditional.tstests/unit/error-handling.test.ts