🧠
Funkcia Docs
🧠
Funkcia Docs
  • Welcome
  • Data types
    • Option
      • Error Propagation
      • Do Notation
    • Result
      • Error Propagation
      • Do Notation
    • OptionAsync
      • Error Propagation
      • Do Notation
    • ResultAsync
      • Error Propagation
      • Do Notation
  • Modules
    • Exceptions
    • Functions
    • JSON
    • Predicate
    • URI
    • URL
Powered by GitBook
On this page

Was this helpful?

Edit on GitHub
  1. Data types
  2. ResultAsync

Error Propagation

PreviousResultAsyncNextDo Notation

Last updated 3 months ago

Was this helpful?

Funkcia offers a concise and convenient way to write your code in a more imperative style that utilizes the native scope provided by the generator syntax. This syntax is more linear and resembles normal synchronous code.

Drawing primarily from Rust's ? operator for error propagation, and inspired by Gleam's , , and functions, the following functions provide a clean way to handle sequential operations while maintaining proper error handling and type safety.

use

Evaluates an async generator early returning when a Result.Error is propagated or returning the ResultAsync returned by the generator.

  • Each yield* automatically awaits and unwraps the ResultAsync value or propagates Error.

  • If any operation resolves to Result.Error, the entire generator exits early.

import { ResultAsync } from 'funkcia';

declare const safeReadFile: (path: string) => ResultAsync;
declare const safeWriteFile: (path: string, content: string) => ResultAsync;

//          β”Œβ”€β”€β”€ ResultAsync
//          β–Ό
const mergedContent = ResultAsync.use(async function* () {
  const  = yield* safeReadFile('data.txt');
  const  = yield* safeReadFile('non-existent-file.txt'); // returns ResultAsync.Error immediately

  return safeWriteFile('output.txt', `${fileA}\n${fileB}`); // doesn't run
});
// Output: Promise

createUse

Returns a function that evaluates an async generator when called with the defined arguments, early returning when a Result.Error is propagated or returning the ResultAsync returned by the generator.

  • Each yield* automatically awaits and unwraps the ResultAsync value or propagates Error.

  • If any operation resolves to Result.Error, the entire generator exits early.

import { ResultAsync } from 'funkcia';

declare const safeReadFile: (path: string) => ResultAsync;
declare const safeWriteFile: (path: string, content: string) => ResultAsync;

//          β”Œβ”€β”€β”€ (output: string, pathA: string, pathB: string) => ResultAsync
//          β–Ό
const safeMergeFiles = ResultAsync.createUse(async function* (output: string, pathA: string, pathB: string) {
  const  = yield* safeReadFile(pathA);
  const  = yield* safeReadFile(pathB);

  return safeWriteFile(output, `${fileA}\n${fileB}`);
});

const mergedContent = safeMergeFiles('output.txt', 'data.txt', 'updated-data.txt');
// Output: Promise

Understanding the use method

The use method provides a way to write sequential operations that might fail, similar to Rust's ? operator. It lets you write code that looks synchronous while safely handling potential failures.

It essentially creates a "safe context" where you can work with values as if they were guaranteed to exist, while maintaining all the safety guarantees of AsyncResult. If anything fails, the failure propagates automatically. Like an electronic relay that controls current flow, ⁠relay controls computation flow: ⁠Result.Ok continues, ⁠Result.Error breaks the circuit.

Here's a practical example:

import { ResultAsync } from 'funkcia';

declare function rateLimit(clientId: ClientId, ip: IpAddress): ResultAsync<ClientId, RateLimitError>;
declare function findUserByEmail(email: Email): ResultAsync<User, UserNotFound>;

const userPreferences = ResultAsync.use(function* () {
  // First, check if API rate limit is allowed
  yield* rateLimit(req.headers['x-client-id'], req.ip);
  // If rate-limit is not blocked, get the user
  const user = yield* findUserByEmail(req.query.email);

  // If all steps succeed, we can use the accumulated context to get user preferences
  return ResultAsync.ok(user.preferences);
});

The equivalent code without use would be much more nested:

import { ResultAsync } from 'funkcia';

declare function rateLimit(clientId: ClientId, ip: IpAddress): ResultAsync<ClientId, RateLimitError>;
declare function findUserByEmail(email: Email): ResultAsync<User, UserNotFound>;

const userPreferences = rateLimit(req.headers['x-client-id'], req.ip)
  .andThen(() =>
    findUserByEmail(req.query.email)
      .map(user => user.preferences)
  );

Or with intermediate variables:

import { ResultAsync } from 'funkcia';

declare function rateLimit(clientId: ClientId, ip: IpAddress): ResultAsync<ClientId, RateLimitError>;
declare function findUserByEmail(email: Email): ResultAsync<User, UserNotFound>;

const rateLimitResult = rateLimit(req.headers['x-client-id'], req.ip);
const user = rateLimitResult.andThen(() => findUserByEmail(req.query.email));

const userPreferences = user.map(user => user.preferences);
use expressions
neverthrow's safeTry
Effect's gen