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.

TypeScript
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()) // true

The 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:

TypeScript
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:

TypeScript
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.

TypeScript
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.

Cleanup can replace the earlier outcome

If finally throws or rejects, that failure propagates instead of the earlier result. Keep cleanup small, and handle its expected failures inside the callback.

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.