Error Propagation
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 use
expressions, neverthrow's safeTry
, and Effect's gen
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 theResultAsync
value or propagatesError
.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 theResultAsync
value or propagatesError
.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);
Last updated
Was this helpful?