Concepts
Catching exceptions with tcf
Turn thrown exceptions and rejected promises into Result values.
On this page
JSON.parse() and many other APIs throw when they fail. Wrap that work in tcf() when you want the caller to receive an Err instead of catching an exception.
For parsing a string in application code, the callback remains synchronous and tcf() returns a Result immediately. Catch input is unknown by default because JavaScript allows throwing any value.
import { tcf } from '@hulla/control'
const parsed = tcf({
try: () => JSON.parse('{"name":"Sam"}') as { name: string },
catch: (cause) => new Error(`Invalid JSON: ${String(cause)}`),
})
console.log(parsed.isOk()) // trueThe cast above describes the expected shape to TypeScript; it does not validate JSON fields at runtime. tcf() catches thrown exceptions, but it is not a schema validator. Separate parsing from shape checks when reading untrusted JSON. This function returns a Result for malformed JSON and for a valid JSON value with the wrong shape:
import { err, ok, tcf, type Result } from '@hulla/control'
type Settings = { title: string }
function parseSettings(source: string): Result<Settings, Error> {
const parsed = tcf({
try: () => JSON.parse(source) as unknown,
catch: (cause) => new Error(`Invalid JSON: ${String(cause)}`),
})
if (parsed.isErr()) return parsed
const value = parsed.value
if (
typeof value !== 'object' ||
value === null ||
!('title' in value) ||
typeof value.title !== 'string'
) {
return err(new Error('Settings must include a string title'))
}
return ok({ title: value.title })
}JSON.parse returns untrusted data; the unknown annotation avoids treating a cast as validation. The property checks establish the Settings shape before constructing Ok. A display caller might recover with a fallback, while a save operation could return the error:
const title = parseSettings(source).match(
(settings) => settings.title,
() => 'Untitled'
)This fallback is appropriate for display. tcf() wraps the parser’s thrown syntax error; explicit err() handles the domain validation failure.
When try, catch, or finally returns a promise, await the tcf() call. The following example runs in a browser and fetches /data.txt from the current site. It keeps a fetch failure inside the Result and returns the response text on success.
const downloaded = await tcf({
try: async () => {
const response = await fetch('/data.txt')
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.text()
},
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
finally: () => console.log('Request finished'),
})
downloaded.match(
(text) => console.log(text),
(error) => console.error(error.message)
)The awaited output is a Result<string, Error>. When every callback is synchronous, tcf() returns immediately. If try, catch, or finally returns a promise, await the call before narrowing.
Cleanup runs once after success or a converted error. A synchronous throw from catch still runs cleanup, then propagates.
For expected domain failures, return err() directly. Read Results to handle the branch.
JavaScript can throw any value, so cause is unknown. Check it before reading properties such as .message. The catch callback must return an Error or a promise of one; that becomes the result’s error value.
You can set tagOk and tagError just as you would on ok() and err(). See Async results for promise handling, or Reference for the exact types.