DeesseJS FP

Try

Wraps synchronous and asynchronous functions that might throw

The Try pattern wraps functions that might throw in a type-safe container. It acts as the bridge between imperative exception handling and functional error management. When you wrap a function with Try, you convert unpredictable exceptions into typed results that TypeScript can track and enforce handling for.

Why Use Try?

JavaScript exceptions can crash your application at runtime. When a function has the ability to throw, TypeScript types do not help you handle the error in a type-safe way. This creates a gap between what the compiler knows and what can actually happen at runtime.

The Problem with Exceptions

Exceptions bypass type checking. A function that parses JSON can throw a SyntaxError, but your TypeScript types do not indicate this possibility. The type system cannot enforce error handling, so crashes can happen silently.

You see this problem when parsing user input or reading files. The type system assumes success, but runtime can throw exceptions that the compiler cannot track.

Here is an example showing how exceptions hide from the type system:

exception-problem.ts
// This can throw, but we do not see it in the type
const data = JSON.parse(userInput); // Can throw SyntaxError
const user = readFileSync(path);     // Can throw on missing file

// No compile-time safety for error handling
console.log(data.name); // Might crash if parse failed

The Try Solution

Try catches exceptions and converts them into typed errors. Instead of a function that might throw, you get a Result type that clearly indicates success or failure. TypeScript can then track and enforce handling for both states.

Here is how Try converts exceptions into typed results:

try-solution.ts
import { attempt, isOk } from '@deessejs/fp';

const result = attempt(() => JSON.parse(userInput));

if (isOk(result)) {
  console.log(result.value); // Safe - TypeScript knows it worked
} else {
  console.error('Parse failed:', result.error.message);
}

Key concepts:

  • Result<T, E> -- Two states: success with value or failure with error
  • attempt(fn) -- Wraps a synchronous function in try/catch
  • attemptAsync(fn) -- Wraps an async function in try/catch
  • isOk() -- Type guard that narrows to the success branch
  • catch -- Optional transform function to map thrown errors to custom types

Core Concepts

Success and Failure

Result<T, E> from Try has two possible states. Understanding these states is fundamental to working with Try effectively.

StateDescriptionProperties
Ok<T>Function succeededok: true, value: T
Err<E>Function threwok: false, error: E

The Ok state contains the successful return value. The Err state contains the error that was thrown during execution. You can use type guards like isOk() and isErr() to narrow between these states safely.

Here is an example showing both states being returned:

success-failure-states.ts
import { attempt } from '@deessejs/fp';

// Success case - the function returns a value
const success = attempt(() => 42);
// { ok: true, value: 42 }

// Failure case - the function throws an error
const failure = attempt(() => {
  throw new Error('Something went wrong');
});
// { ok: false, error: Error: 'Something went wrong' }

Wrapping Behavior

Try handles non-Error throws by converting them to Error objects. This ensures consistent error handling regardless of what type of value is thrown.

When you throw a string, number, or object, Try wraps it in an Error for uniform handling. Strings become Error with the string as the message. Numbers become Error with the number converted to a string. Objects get stringified.

Here are examples of this wrapping behavior:

wrapping-behavior.ts
// Strings are wrapped in Error
attempt(() => { throw 'oops'; });     // Err(Error: 'oops')

// Numbers are wrapped in Error
attempt(() => { throw 42; });         // Err(Error: '42')

// Objects are stringified
attempt(() => { throw { code: 500 }; }); // Err(Error: '[object Object]')

Reference

Creating Tries

attempt function

This function wraps a synchronous function in try/catch. It returns Ok if the function succeeds, or Err if the function throws. This is the primary way to convert code that might throw into type-safe code.

The fn parameter is a function that takes no arguments and returns a value. It will be executed inside a try/catch block. Any exception thrown by fn will be caught and converted to an Err result.

attempt-basic.ts
import { attempt } from '@deessejs/fp';

attempt(() => 42);                           // Ok(42)
attempt(() => { throw new Error('oops'); });  // Err(Error: 'oops')

attempt with catchFn

This overload lets you provide a custom error handler. The first parameter is the function to wrap, and the second parameter is a transform function that receives the caught error and returns a custom error type.

The catchFn parameter is a function that takes the caught error (of type unknown) and returns an error of your custom type. This lets you map raw exceptions to structured errors that your application can handle meaningfully.

attempt-with-catch.ts
import { attempt, error } from '@deessejs/fp';
import { z } from 'zod';

const ParseError = error({
  name: 'ParseError',
  schema: z.object({ message: z.string() }),
  message: (args) => `Parse failed: ${args.message}`,
});

const parseJson = (input: string) =>
  attempt(
    () => JSON.parse(input),
    (cause) => ParseError({ message: cause instanceof Error ? cause.message : 'Unknown' })
  );

// Usage
const result = parseJson('{"valid": true}');
if (result.ok) {
  console.log(result.value); // { valid: true }
}

attemptAsync function

This function wraps an async function in try/catch. It returns Promise<Result<T, E>>. When the async function succeeds, the promise resolves to Ok with the value. When the async function throws, the promise resolves to Err with the error.

The fn parameter is an async function (or a function that returns a Promise) that you want to wrap. This lets you safely call async operations that might throw.

attempt-async-basic.ts
import { attemptAsync } from '@deessejs/fp';

const result = await attemptAsync(async () => {
  const response = await fetch('https://api.example.com');
  return response.json();
});

attemptAsync with catchFn

This overload lets you provide a custom error handler for async functions. The first parameter is the async function to wrap, and the second parameter transforms caught errors into your custom error type.

The catchFn parameter receives the caught error and should return a custom error. This is useful for mapping network errors, HTTP status errors, or validation failures into your application's error types.

attempt-async-with-catch.ts
import { attemptAsync, error } from '@deessejs/fp';
import { z } from 'zod';

const NetworkError = error({
  name: 'NetworkError',
  schema: z.object({ url: z.string(), status: z.number().optional() }),
  message: (args) => `Failed to fetch: ${args.url}`,
});

const fetchData = (url: string) =>
  attemptAsync(
    async () => {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json();
    },
    (cause) => NetworkError({ url })
  );

Type Guards

isOk function

This function is a type guard that narrows a Result to the Ok branch. When you call isOk(result) and it returns true, TypeScript knows the result is Ok and you can safely access result.value.

The result parameter is the Result you want to test. The function returns true if the result is Ok, and false if the result is Err. This is useful for conditional logic that needs to handle success and failure cases differently.

is-ok-usage.ts
import { attempt, isOk } from '@deessejs/fp';

const result = attempt(() => 42);

if (isOk(result)) {
  console.log(result.value); // 42
}

isErr function

This function is a type guard that narrows a Result to the Err branch. When you call isErr(result) and it returns true, TypeScript knows the result is Err and you can safely access result.error.

The result parameter is the Result you want to test. The function returns true if the result is Err, and false if the result is Ok. This is useful for error handling paths where you need to access error details.

is-err-usage.ts
import { attempt, isErr } from '@deessejs/fp';

const result = attempt(() => { throw new Error('oops'); });

if (isErr(result)) {
  console.log(result.error.message); // 'oops'
}

Transformation

map function

This function transforms the success value while passing failures through unchanged. It takes a Result and a mapping function. If the result is Ok, the mapping function is applied to the value. If the result is Err, the error passes through unchanged.

The result parameter is the Result to transform. The fn parameter is a mapping function that receives the success value and returns a new value of potentially a different type. Failures are not affected by the mapping function.

map-usage.ts
import { attempt, map } from '@deessejs/fp';

map(attempt(() => 2), x => x * 2);                           // Ok(4)
map(attempt(() => { throw new Error(); }), x => x * 2);     // Err(Error)

The map function is also available as a method on the Result type for fluent chaining:

map-method.ts
attempt(() => 2).map(x => x * 2); // Ok(4)

flatMap function

This function chains operations that can throw together. If the first result is Ok, the mapping function is applied to the value. If the first result is Err, the error passes through without calling the mapping function.

The result parameter is the starting Result. The fn parameter is a function that receives the success value and returns a new Result. This lets you compose multiple throwing operations while maintaining type safety.

flatmap-usage.ts
import { attempt, flatMap } from '@deessejs/fp';

const parseNumber = (s: string) =>
  attempt(() => {
    const n = parseInt(s, 10);
    if (isNaN(n)) throw new Error('Invalid number');
    return n;
  });

flatMap(attempt(() => '21'), parseNumber);   // Ok(21)
flatMap(attempt(() => 'abc'), parseNumber);  // Err(Error: 'Invalid number')

The flatMap function is also available as a method on the Result type for fluent chaining:

flatmap-method.ts
attempt(() => '21').flatMap(parseNumber); // Ok(21)

Extraction

getOrElse function

This function returns the success value if the result is Ok. If the result is Err, it returns the provided default value instead. This gives you a simple way to handle failures with fallback values.

The result parameter is the Result to extract from. The defaultValue parameter is the value to return if the result is Err. The default value is returned as-is, so choose a value that makes sense for your use case.

get-or-else-usage.ts
import { attempt, getOrElse } from '@deessejs/fp';

getOrElse(attempt(() => 42), 0);                            // 42
getOrElse(attempt(() => { throw new Error(); }), 0);         // 0

The getOrElse function is also available as a method on the Result type:

get-or-else-method.ts
attempt(() => 42).getOrElse(0); // 42

getOrCompute function

This function returns the success value if the result is Ok. If the result is Err, it lazily computes a fallback value by calling the provided function. This is useful when the fallback value is expensive to create and you only want to compute it when needed.

The result parameter is the Result to extract from. The fn parameter is a function that returns the fallback value. This function is only called if the result is Err, which avoids unnecessary computation when the operation succeeds.

get-or-compute-usage.ts
import { attempt, getOrCompute } from '@deessejs/fp';

const expensive = () => { console.log('computing...'); return 0; };

getOrCompute(attempt(() => 42), expensive);                            // 42 (never logs)
getOrCompute(attempt(() => { throw new Error(); }), expensive);         // logs 'computing...', returns 0

The getOrCompute function is also available as a method on the Result type:

get-or-compute-method.ts
attempt(() => { throw new Error(); }).getOrCompute(() => 0); // 0

Side Effects

tap function

This function executes a side effect on the value without changing the result. It passes through both the value and errors unchanged. This is useful for logging, debugging, or triggering side effects that should not affect the result.

The result parameter is the Result to tap. The fn parameter is a function that receives the value and performs a side effect. The function is only called if the result is Ok. Errors pass through without triggering the side effect.

tap-usage.ts
import { attempt, tap } from '@deessejs/fp';

tap(attempt(() => 42), x => console.log('Got:', x));                 // Logs: 'Got: 42', returns Ok(42)
tap(attempt(() => { throw new Error(); }), x => console.log('Got:', x)); // Nothing logged, returns Err(Error)

The tap function is also available as a method on the Result type:

tap-method.ts
attempt(() => 42).tap(x => console.log(x)); // Ok(42)

tapErr function

This function executes a side effect on the error without changing the result. It passes through both the value and errors unchanged. This is useful for logging errors or triggering side effects when an operation fails.

The result parameter is the Result to tap. The fn parameter is a function that receives the error and performs a side effect. The function is only called if the result is Err. Success values pass through without triggering the side effect.

tap-err-usage.ts
import { attempt, tapErr } from '@deessejs/fp';

tapErr(attempt(() => 42), e => console.error('Error:', e));                            // Nothing logged
tapErr(attempt(() => { throw new Error('oops'); }), e => console.error('Error:', e)); // Logs: 'Error: Error: oops'

The tapErr function is also available as a method on the Result type:

tap-err-method.ts
attempt(() => { throw new Error('oops'); }).tapErr(e => console.error(e)); // Err(Error: 'oops')

Pattern Matching

match function

This function handles both success and failure cases by applying the appropriate function based on the result state. It always returns a single value, making it a complete solution for handling results.

The result parameter is the Result to match on. The onOk parameter is a function that receives the value and returns a value of the type you need. The onErr parameter is a function that receives the error and returns a value of the same type. One of these functions will be called, never both.

match-usage.ts
import { attempt, match } from '@deessejs/fp';

match(
  attempt(() => 42),
  value => `Success: ${value}`,
  error => `Failed: ${error.message}`
); // "Success: 42"

match(
  attempt(() => { throw new Error('oops'); }),
  value => `Success: ${value}`,
  error => `Failed: ${error.message}`
); // "Failed: oops"

You can return different types from each branch, which is useful for converting results to the type you need:

match-different-types.ts
match(
  attempt(() => 'hello'),
  value => value.length,  // string -> number
  () => -1               // fallback
); // 5

Conversions

toNullable function

This function converts a Result to a nullable value. If the result is Ok, it returns the value. If the result is Err, it returns null. This lets you integrate Try with code that expects nullable types.

The result parameter is the Result to convert. The function returns T | null where T is the success value type.

to-nullable-usage.ts
import { attempt, toNullable } from '@deessejs/fp';

toNullable(attempt(() => 42));                         // 42
toNullable(attempt(() => { throw new Error(); }));    // null

toUndefined function

This function converts a Result to an optional undefined value. If the result is Ok, it returns the value. If the result is Err, it returns undefined. This lets you integrate Try with code that expects optional types.

The result parameter is the Result to convert. The function returns T | undefined where T is the success value type.

to-undefined-usage.ts
import { attempt, toUndefined } from '@deessejs/fp';

toUndefined(attempt(() => 42));                         // 42
toUndefined(attempt(() => { throw new Error(); }));     // undefined

Method Chaining

Try objects support chaining for fluent transformations. All transformation functions like map, flatMap, tap, and tapErr are available as methods on the Result type. This lets you chain multiple operations together in a readable way.

Each method in the chain returns a new Result, so you can continue chaining as long as you need. If any step in the chain produces an Err, all subsequent transformations are skipped and the error passes through.

Here is an example of chaining multiple transformations:

method-chaining.ts
import { attempt } from '@deessejs/fp';

const result = attempt(() => 'hello')
  .map(s => s.toUpperCase())           // Ok('HELLO')
  .map(s => s + '!')                  // Ok('HELLO!')
  .flatMap(s => attempt(() => {
    if (s.length > 5) return s;
    throw new Error('Too short');
  }));                                 // Ok('HELLO!')

Real-World Examples

JSON Parsing

Parsing user input or file contents can throw exceptions. Try lets you handle these failures gracefully and provide sensible defaults or clear error messages.

This example shows how to wrap JSON parsing with Try and handle the result with a default fallback:

json-parsing.ts
import { attempt, getOrElse, error } from '@deessejs/fp';
import { z } from 'zod';

const ParseError = error({
  name: 'ParseError',
  schema: z.object({ message: z.string() }),
});

const parseUser = (json: string) =>
  attempt(
    () => JSON.parse(json) as { id: number; name: string },
    (cause) => ParseError({ message: cause instanceof Error ? cause.message : 'Unknown' })
  );

const user = getOrElse(
  parseUser('{"id": 1, "name": "Alice"}'),
  { id: 0, name: 'Guest' }
);
// { id: 1, name: 'Alice' }

File Operations

Reading files can fail for many reasons: missing file, permission denied, invalid encoding. Try wraps file operations so you can handle these failures at the call site rather than letting exceptions crash your program.

This example shows how to safely read and parse a JSON configuration file:

file-operations.ts
import { attempt } from '@deessejs/fp';
import { readFileSync } from 'fs';

const readJsonFile = (path: string) =>
  attempt(() => JSON.parse(readFileSync(path, 'utf-8')));

const config = readJsonFile('./config.json');

if (config.ok) {
  console.log('Config loaded:', config.value);
} else {
  console.error('Failed:', config.error.message);
}

Async API Calls

Network requests can fail for many reasons: server errors, timeouts, invalid responses. Try combined with attemptAsync gives you type-safe async operations that you can handle at the call site.

This example shows how to safely fetch data from an API with custom error handling:

async-api-calls.ts
import { attemptAsync, isOk, getOrElse, error } from '@deessejs/fp';
import { z } from 'zod';

const NetworkError = error({
  name: 'NetworkError',
  schema: z.object({ url: z.string() }),
});

const fetchPost = (id: number) =>
  attemptAsync(
    async () => {
      const response = await fetch(`/api/posts/${id}`);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return response.json() as Promise<{ id: number; title: string }>;
    },
    (cause) => NetworkError({ url: `/api/posts/${id}` })
  );

const post = await fetchPost(1);

if (isOk(post)) {
  console.log(post.value.title);
} else {
  console.error('Failed to fetch post:', post.error.message);
}

// Or use getOrElse for a fallback
const safePost = await getOrElse(fetchPost(1), { id: 0, title: 'Untitled' });

Try vs Result

Understanding when to use Try versus Result helps you choose the right tool for your error handling needs. Both represent success or failure, but they differ in how errors are captured and when to use each.

AspectTryResult
Error sourceCatches thrown exceptionsYou return err() explicitly
Error typeDefault Error, or custom via handlerFully typed by you
Use caseWrapping legacy/third-party codeOperations with defined error types

Use Try when you are wrapping code that might throw, especially third-party libraries or legacy code. Use Result when you want complete control over your error types and want to explicitly return errors from your functions.

try-vs-result.ts
// Try: for things that might throw
const parsed = attempt(() => JSON.parse(input));

// Result: for operations with explicit error types
const validated = validateUser(input); // Returns Result<User, ValidationError>

In v4.0.0, Try is not a separate type. Use Result<T, E> with attempt() for synchronous operations and attemptAsync() for async operations.

See Also

On this page