Skip to content

Functional Utilities

The awaitly/functional module provides Effect-inspired functional utilities for Result types. It includes pipe-based composition, Result combinators, and collection utilities with automatic error short-circuiting.

Functional utilities enable declarative composition of Result-returning operations. Instead of nested conditionals or method chaining, you can use pipe() to compose operations left-to-right with automatic error propagation.

import { pipe, R } from 'awaitly/functional';
import { ok, err } from 'awaitly';
// Transform and chain operations
const result = pipe(
ok(5),
R.map(x => x * 2), // 10
R.flatMap(x => x > 0 ? ok(x) : err('NEGATIVE')),
R.tap(x => console.log(x)), // Side effect: logs 10
R.map(x => x + 1) // 11
);

For tagged-union pattern matching (e.g. event handlers or discriminated unions), use awaitly/core: Match, matchTag, matchWhen, MatchTag, and related exports. See Pattern matching (awaitly/core) in the API reference.

Pipe a value through a series of functions left-to-right:

import { pipe } from 'awaitly/functional';
const result = pipe(
5,
(x) => x * 2, // 10
(x) => x + 1 // 11
); // 11

With Result types:

import { pipe, R } from 'awaitly/functional';
import { ok } from 'awaitly';
const result = pipe(
ok(5),
R.map(x => x * 2),
R.map(x => x + 1)
); // ok(11)

Compose functions left-to-right, returning a new function:

import { flow } from 'awaitly/functional';
const double = (x: number) => x * 2;
const addOne = (x: number) => x + 1;
const transform = flow(double, addOne);
transform(5); // 11

Useful for creating reusable transformations:

import { flow, R } from 'awaitly/functional';
import { ok } from 'awaitly';
const processNumber = flow(
R.map((x: number) => x * 2),
R.map((x: number) => x + 1)
);
const result = processNumber(ok(5)); // ok(11)

Compose functions right-to-left (opposite of flow):

import { compose } from 'awaitly/functional';
const double = (x: number) => x * 2;
const addOne = (x: number) => x + 1;
const transform = compose(addOne, double);
transform(5); // 11 (double first, then addOne)

Identity function - returns its argument unchanged:

import { identity } from 'awaitly/functional';
identity(42); // 42

Useful as a placeholder in compositions or for default transformations.

Transform the success value:

import { map } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const result = ok(5);
map(result, (x) => x * 2); // ok(10)
const error = err("not found");
map(error, (x) => x * 2); // err("not found") - unchanged

Transform and flatten (short-circuits on error):

import { flatMap } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const divide = (a: number, b: number) =>
b === 0 ? err("division by zero") : ok(a / b);
const result = ok(10);
flatMap(result, (x) => divide(x, 2)); // ok(5)
flatMap(result, (x) => divide(x, 0)); // err("division by zero")

Transform both success and error values:

import { bimap } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const result = ok(5);
bimap(result, (x) => x * 2, (e) => `Error: ${e}`); // ok(10)
const error = err("not found");
bimap(error, (x) => x * 2, (e) => `Error: ${e}`); // err("Error: not found")

Transform the error value:

import { mapError } from 'awaitly/functional';
import { err } from 'awaitly';
const error = err("not found");
mapError(error, (e) => ({ type: "ERROR", message: e }));
// err({ type: "ERROR", message: "not found" })

Side effect on success (returns original result):

import { tap } from 'awaitly/functional';
import { ok } from 'awaitly';
const result = ok(5);
tap(result, (x) => console.log(`Value: ${x}`)); // logs "Value: 5", returns ok(5)

Side effect on error (returns original result):

import { tapError } from 'awaitly/functional';
import { err } from 'awaitly';
const error = err("not found");
tapError(error, (e) => console.log(`Error: ${e}`)); // logs "Error: not found", returns err

Pattern match on Result:

import { match } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const result = ok(5);
match(result, {
ok: (x) => `Success: ${x}`,
err: (e) => `Error: ${e}`
}); // "Success: 5"
const error = err("not found");
match(error, {
ok: (x) => `Success: ${x}`,
err: (e) => `Error: ${e}`
}); // "Error: not found"

Recover from error by providing fallback value:

import { recover } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const error = err("not found");
recover(error, () => 0); // 0
const success = ok(5);
recover(success, () => 0); // 5

Recover from error with another Result:

import { recoverWith } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const error = err("not found");
recoverWith(error, (e) => ok(0)); // ok(0)
recoverWith(error, (e) => err("still failed")); // err("still failed")

Get the value or a default:

import { getOrElse } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const error = err("not found");
getOrElse(error, 0); // 0
const success = ok(5);
getOrElse(success, 0); // 5

Get the value or compute a default lazily:

import { getOrElseLazy } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const error = err("not found");
getOrElseLazy(error, () => expensiveComputation()); // calls expensiveComputation()
const success = ok(5);
getOrElseLazy(success, () => expensiveComputation()); // 5, doesn't call expensiveComputation

Transform success value asynchronously:

import { mapAsync } from 'awaitly/functional';
import { ok } from 'awaitly';
const result = ok(5);
await mapAsync(result, async (x) => x * 2); // ok(10)

Async flatMap:

import { flatMapAsync } from 'awaitly/functional';
import { ok, type AsyncResult } from 'awaitly';
const fetchUser = async (id: string): AsyncResult<User, "NOT_FOUND"> => {
// ... fetch logic
};
const result = ok("user-123");
await flatMapAsync(result, fetchUser); // AsyncResult<User, "NOT_FOUND">

Async side effect on success:

import { tapAsync } from 'awaitly/functional';
import { ok } from 'awaitly';
const result = ok(5);
await tapAsync(result, async (x) => {
await logToServer(x);
}); // ok(5)

Async side effect on error:

import { tapErrorAsync } from 'awaitly/functional';
import { err } from 'awaitly';
const error = err("not found");
await tapErrorAsync(error, async (e) => {
await logErrorToServer(e);
}); // err("not found")

Combine array of Results - fails fast on first error:

import { all } from 'awaitly/functional';
import { ok, err } from 'awaitly';
all([ok(1), ok(2), ok(3)]); // ok([1, 2, 3])
all([ok(1), err("fail"), ok(3)]); // err("fail")

Combine array of AsyncResults - parallel execution, fails fast:

import { allAsync } from 'awaitly/functional';
import { ok, type AsyncResult } from 'awaitly';
const fetchUser = async (id: string): AsyncResult<User, "NOT_FOUND"> => {
// ... fetch logic
};
await allAsync([
fetchUser("1"),
fetchUser("2"),
fetchUser("3")
]); // AsyncResult<User[], "NOT_FOUND">

Returns immediately when any result fails, without waiting for pending promises.

Collect all results, separating successes and failures:

import { allSettled } from 'awaitly/functional';
import { ok, err } from 'awaitly';
allSettled([ok(1), err("a"), ok(2), err("b")]);
// { ok: [1, 2], err: ["a", "b"] }

Async version of allSettled:

import { allSettledAsync } from 'awaitly/functional';
await allSettledAsync([
fetchUser("1"),
fetchUser("2"),
fetchUser("3")
]); // { ok: [...users], err: [...errors] }

Return first success, or all errors if all fail:

import { any } from 'awaitly/functional';
import { ok, err } from 'awaitly';
any([err("a"), ok(1), err("b")]); // ok(1)
any([err("a"), err("b"), err("c")]); // err(["a", "b", "c"])

Async version of any - returns first success immediately:

import { anyAsync } from 'awaitly/functional';
await anyAsync([
fetchFromCache(key),
fetchFromDb(key),
fetchFromApi(key)
]); // First successful result

Race async results - first to complete wins:

import { race } from 'awaitly/functional';
await race([
fetchFromPrimaryServer(id),
fetchFromBackupServer(id)
]); // Result from whichever server responds first

Sequence an array through a Result-returning function. Stops on first error:

import { traverse } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const validate = (x: number) =>
x > 0 ? ok(x) : err("must be positive");
traverse([1, 2, 3], validate); // ok([1, 2, 3])
traverse([1, -2, 3], validate); // err("must be positive")

Async version of traverse:

import { traverseAsync } from 'awaitly/functional';
await traverseAsync(userIds, async (id) => fetchUser(id));

Parallel traverse - executes all in parallel, fails fast:

import { traverseParallel } from 'awaitly/functional';
await traverseParallel(userIds, fetchUser);

Returns immediately when any result fails, without waiting for pending operations.

The R namespace provides curried versions of all Result combinators for use in pipe():

import { pipe, R } from 'awaitly/functional';
import { ok } from 'awaitly';
const result = pipe(
fetchUser(id),
R.flatMap(user => fetchPosts(user.id)),
R.map(posts => posts.filter(p => p.published)),
R.tap(posts => console.log(`Found ${posts.length} posts`)),
R.match({
ok: posts => `Found ${posts.length} posts`,
err: error => `Failed: ${error}`
})
);
  • R.map(fn) - Transform value
  • R.flatMap(fn) - Chain operations
  • R.bimap(onOk, onErr) - Transform both value and error
  • R.mapError(fn) - Transform error
  • R.tap(fn) - Side effect on success
  • R.tapError(fn) - Side effect on error
  • R.match(patterns) - Pattern match
  • R.recover(fn) - Recover with fallback value
  • R.recoverWith(fn) - Recover with Result
  • R.getOrElse(defaultValue) - Get value or default
  • R.getOrElseLazy(fn) - Get value or compute default
import { pipe, R } from 'awaitly/functional';
import { ok, err, type AsyncResult } from 'awaitly';
type User = { id: string; name: string };
type Post = { id: string; userId: string; title: string };
const fetchUser = async (id: string): AsyncResult<User, "NOT_FOUND"> => {
// ... fetch logic
};
const fetchPosts = async (userId: string): AsyncResult<Post[], "FETCH_ERROR"> => {
// ... fetch logic
};
const result = await pipe(
fetchUser("123"),
R.flatMapAsync(user => fetchPosts(user.id)),
R.map(posts => posts.filter(p => p.title.length > 10)),
R.tapAsync(async posts => {
await logAnalytics({ postCount: posts.length });
}),
R.map(posts => ({ count: posts.length, posts }))
);
import { pipe, R, traverse } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const validateEmail = (email: string) =>
email.includes("@") ? ok(email) : err("INVALID_EMAIL");
const validateAge = (age: number) =>
age >= 18 ? ok(age) : err("UNDERAGE");
const validateUser = (user: { email: string; age: number }) =>
pipe(
ok(user),
R.flatMap(u => validateEmail(u.email)),
R.flatMap(() => validateAge(user.age)),
R.map(() => user)
);
const validateUsers = (users: Array<{ email: string; age: number }>) =>
traverse(users, validateUser);
import { pipe, R } from 'awaitly/functional';
import { ok, err } from 'awaitly';
const fetchFromPrimary = (): AsyncResult<Data, "PRIMARY_ERROR"> => {
// ... fetch logic
};
const fetchFromFallback = (): AsyncResult<Data, "FALLBACK_ERROR"> => {
// ... fetch logic
};
const result = await pipe(
fetchFromPrimary(),
R.recoverWith(() => fetchFromFallback()),
R.mapError(e => ({ type: "FETCH_FAILED", source: e })),
R.tapError(e => console.error("Fetch failed:", e))
);

Functional utilities provide an alternative to method chaining:

// Method chaining (not available in awaitly)
// result.map(x => x * 2).flatMap(x => ok(x + 1))
// Functional style
import { pipe, R } from 'awaitly/functional';
pipe(
result,
R.map(x => x * 2),
R.flatMap(x => ok(x + 1))
)

All functional utilities preserve TypeScript types:

import { pipe, R } from 'awaitly/functional';
import { ok, err, type Result } from 'awaitly';
const result: Result<number, "NOT_FOUND" | "INVALID"> = ok(5);
const transformed = pipe(
result,
R.map(x => x * 2), // Result<number, "NOT_FOUND" | "INVALID">
R.flatMap(x => x > 0 ? ok(x) : err("INVALID" as const))
// Result<number, "NOT_FOUND" | "INVALID">
);

TypeScript infers error unions automatically across compositions.

  1. Use pipe() for linear compositions - Clean left-to-right flow
  2. Use R namespace in pipes - Curried functions work seamlessly
  3. Use flow() for reusable transformations - Create composable functions
  4. Prefer getOrElseLazy() over getOrElse() - Avoid unnecessary computation
  5. Use allAsync() for parallel operations - Automatic error short-circuiting
  6. Use traverse() for sequential validation - Stops on first error