AsyncResult
Async error handling with Thenable interface for chained async operations
The AsyncResult type is the asynchronous version of Result. It provides a Thenable interface that allows chaining async operations without intermediate await calls, while maintaining proper error handling.
Why AsyncResult?
Async/await is great, but error handling with try/catch is error-prone. AsyncResult makes async errors explicit in your types.
The Problem with Try/Catch
// Easy to forget to catch errors
const user = await fetchUser(id);
// If fetchUser throws, we have an unhandled rejectionThe AsyncResult Solution
import { fromPromise, isOk } from '@deessejs/fp';
const result = fromPromise(fetchUser(id));
if (isOk(result)) {
console.log(result.value);
} else {
console.error(result.error);
}Key concepts:
AsyncResult<T, E>- Async success/failure with Thenable interfacefromPromise(promise)- Wraps a Promise, catching rejections asPanicErrorawaitdirectly - AsyncResult is Thenable, so useawaitdirectly on itmap,flatMap- Handle both sync and async transformation functionsall,race,traverse- Combine multiple async operations
Core Concepts
AsyncOk and AsyncErr
AsyncResult<T, E> has two possible states:
| State | Description | Properties |
|---|---|---|
AsyncOk<T> | Async success | ok: true, value: T |
AsyncErr<E> | Async failure | ok: false, error: E |
import { okAsync, errAsync } from '@deessejs/fp';
// Success
const success = await okAsync({ id: 1, name: 'John' });
// { ok: true, value: { id: 1, name: 'John' } }
// Failure
const failure = await errAsync({ code: 'NOT_FOUND', message: 'User not found' });
// { ok: false, error: { code: 'NOT_FOUND', message: 'User not found' } }Thenable Interface
AsyncResult implements the Thenable pattern, allowing direct await and chaining:
import { fromPromise } from '@deessejs/fp';
// Direct await
const result = await fromPromise(fetch('/api/user'));
// Thenable chaining without intermediate await
fromPromise(fetch('/api/user'))
.then(result => result.ok ? result.value : null)
.catch(error => null);Error Wrapping
When using fromPromise, rejections are automatically wrapped:
- Error objects are wrapped in
PanicErrorwith the original error as cause - Non-Error values are stringified and wrapped in
PanicError
// String rejections are wrapped
fromPromise(Promise.reject('oops'));
// AsyncErr(PanicError({ message: 'oops' }))
// Error rejections are preserved as cause
fromPromise(Promise.reject(new Error('failed')));
// AsyncErr(PanicError({ message: 'failed' }).from(originalError))Reference
Creating AsyncResults
ok(value) / okAsync(value)
Creates an async success result.
import { ok, okAsync } from '@deessejs/fp';
ok(42); // AsyncOk(42)
okAsync('hello'); // AsyncOk('hello')err(error) / errAsync(error)
Creates an async error result.
import { err, errAsync } from '@deessejs/fp';
err('Something went wrong'); // AsyncErr('Something went wrong')
errAsync({ code: 'NOT_FOUND' }); // AsyncErr({ code: 'NOT_FOUND' })fromPromise(promise)
Wraps a Promise. Resolves to AsyncOk if the promise succeeds, AsyncErr if it rejects.
import { fromPromise } from '@deessejs/fp';
const result = await fromPromise(fetch('/api/user'));fromPromise(promise, onError)
Wraps a Promise with a custom error handler.
import { fromPromise, error } from '@deessejs/fp';
import { z } from 'zod';
const NetworkError = error({
name: 'NetworkError',
schema: z.object({ url: z.string() }),
message: (args) => `Failed to fetch: ${args.url}`,
});
const result = fromPromise(
fetch('/api/user'),
(err) => NetworkError({ url: '/api/user' })
);fromPromise(promise, { signal })
Wraps a Promise with AbortSignal support.
import { fromPromise } from '@deessejs/fp';
const controller = new AbortController();
const result = fromPromise(
fetch('/api/user', { signal: controller.signal }),
{ signal: controller.signal }
);fromValue(value, ms)
Creates an AsyncResult that resolves after a delay.
import { fromValue } from '@deessejs/fp';
fromValue(42, 1000); // Resolves to AsyncOk(42) after 1 secondfromError(error, ms)
Creates an AsyncResult that rejects after a delay.
import { fromError } from '@deessejs/fp';
fromError({ code: 'TIMEOUT' }, 1000); // Resolves to AsyncErr({ code: 'TIMEOUT' }) after 1 secondType Guards
isOk(result)
Type guard that narrows to the AsyncOk branch.
import { fromPromise, isOk } from '@deessejs/fp';
const result = await fromPromise(Promise.resolve(42));
if (isOk(result)) {
console.log(result.value); // 42
}isErr(result)
Type guard that narrows to the AsyncErr branch.
import { fromPromise, isErr } from '@deessejs/fp';
const result = await fromPromise(Promise.reject('oops'));
if (isErr(result)) {
console.log(result.error); // 'oops'
}isAbortError(error)
Checks if an error is an AbortError.
import { isAbortError } from '@deessejs/fp';
if (isAbortError(error)) {
console.log('Operation was aborted');
}Transformation
map(result, fn)
Transforms the value if AsyncOk. Handles both sync and async functions. Passes errors through unchanged.
import { fromPromise, map } from '@deessejs/fp';
// Sync transformation
map(ok(2), x => x * 2); // AsyncOk(4)
// Async transformation (map handles async functions automatically)
map(ok(2), async x => {
const data = await fetchData(x);
return data.value * 2;
});Also available as a method:
ok(2).map(x => x * 2); // AsyncOk(4)flatMap(result, fn)
Chains AsyncResults. If AsyncOk, applies the function. If AsyncErr, returns AsyncErr. Handles both sync and async functions.
import { ok, flatMap } from '@deessejs/fp';
const fetchUser = (id: number) =>
id > 0 ? ok({ id, name: 'John' }) : err('Invalid id');
flatMap(ok(1), fetchUser); // AsyncOk({ id: 1, name: 'John' })
flatMap(ok(-1), fetchUser); // AsyncErr('Invalid id')Also available as a method:
ok(1).flatMap(fetchUser); // AsyncOk({ id: 1, name: 'John' })mapErr(result, fn)
Transforms the error. Passes success through unchanged.
import { err, mapErr } from '@deessejs/fp';
mapErr(err('not found'), e => ({ ...e, code: 'NOT_FOUND' }));
// AsyncErr({ ...original error, code: 'NOT_FOUND' })Also available as a method:
err('not found').mapErr(e => ({ ...e, code: 'NOT_FOUND' }));Extraction
getOrElse(result, defaultValue)
Returns the value if AsyncOk, or a default if AsyncErr.
import { ok, err, getOrElse } from '@deessejs/fp';
getOrElse(ok(42), 0); // 42
getOrElse(err('oops'), 0); // 0Also available as a method:
ok(42).getOrElse(0); // 42getOrCompute(result, fn)
Returns the value if AsyncOk, or computes one lazily if AsyncErr.
import { ok, err, getOrCompute } from '@deessejs/fp';
const expensive = () => { console.log('computing...'); return 0; };
getOrCompute(ok(42), expensive); // 42 (never logs)
getOrCompute(err('oops'), expensive); // logs 'computing...', returns 0Also available as a method:
err('oops').getOrCompute(() => 0); // 0unwrap(result)
Extracts the value if AsyncOk, throws the error if AsyncErr.
import { ok, err, unwrap } from '@deessejs/fp';
unwrap(ok(42)); // 42
unwrap(err('oops')); // throws 'oops'Also available as a method:
ok(42).unwrap(); // 42unwrapOr(result, defaultValue)
Extracts the value if AsyncOk, returns default if AsyncErr.
import { ok, err, unwrapOr } from '@deessejs/fp';
unwrapOr(ok(42), 0); // 42
unwrapOr(err('oops'), 0); // 0Side Effects
tap(result, fn)
Executes a function on the value without changing it. Useful for logging.
import { ok, tap } from '@deessejs/fp';
tap(ok(42), x => console.log('Got:', x)); // Logs: 'Got: 42', returns AsyncOk(42)
tap(err('oops'), x => console.log('Got:', x)); // Nothing logged, returns AsyncErr('oops')Also available as a method:
ok(42).tap(x => console.log(x)); // AsyncOk(42)tapErr(result, fn)
Executes a function on the error without changing it.
import { err, tapErr } from '@deessejs/fp';
tapErr(ok(42), e => console.error('Error:', e)); // Nothing logged
tapErr(err('oops'), e => console.error('Error:', e)); // Logs: 'Error: oops'Also available as a method:
err('oops').tapErr(e => console.error(e)); // AsyncErr('oops')Pattern Matching
match(result, okFn, errFn)
Handles both AsyncOk and AsyncErr cases, returning a single Promise value.
import { ok, match } from '@deessejs/fp';
match(
ok(42),
value => `Success: ${value}`,
error => `Failed: ${error}`
); // Promise<'Success: 42'>Conversions
toNullable(result)
Converts to Promise<T | null>.
import { ok, err, toNullable } from '@deessejs/fp';
toNullable(ok(42)); // Promise<42>
toNullable(err('x')); // Promise<null>toUndefined(result)
Converts to Promise<T | undefined>.
import { ok, err, toUndefined } from '@deessejs/fp';
toUndefined(ok(42)); // Promise<42>
toUndefined(err('x')); // Promise<undefined>Combination
all(...results)
Runs multiple AsyncResults in parallel. Returns AsyncOk with array of values if all succeed, or the first AsyncErr (fail-fast).
import { fromPromise, all } from '@deessejs/fp';
all(
fromPromise(fetch('/api/user/1')),
fromPromise(fetch('/api/user/2')),
fromPromise(fetch('/api/user/3'))
); // AsyncOk([user1, user2, user3]) or AsyncErr(firstError)allSettled(...results)
Runs multiple AsyncResults in parallel. Returns AsyncOk with tuple of values and errors, regardless of success or failure.
import { fromPromise, allSettled } from '@deessejs/fp';
allSettled(
fromPromise(fetch('/api/user/1')),
fromPromise(fetch('/api/user/2'))
); // AsyncOk({ values: [user1, user2], errors: [] })
// or if both fail: AsyncOk({ values: [], errors: [err1, err2] })race(...results)
Returns the value of the first AsyncResult to resolve. Throws if the first one rejects.
import { fromPromise, race, errAsync } from '@deessejs/fp';
race(
fromPromise(fetch('/api/fast')),
fromPromise(fetch('/api/slow'))
); // Resolves to the fastest response, or throws if first rejectstraverse(items, fn)
Maps over items in parallel, running an async function for each.
import { fromPromise, traverse } from '@deessejs/fp';
const ids = [1, 2, 3];
traverse(ids, id =>
fromPromise(fetch(`/api/users/${id}`).then(r => r.json()))
); // AsyncOk([user1, user2, user3]) or AsyncErr(firstError)Signal Handling
withSignal(result, signal)
Wraps an AsyncResult to abort when the AbortSignal is triggered.
import { fromPromise, withSignal } from '@deessejs/fp';
const controller = new AbortController();
const result = withSignal(
fromPromise(fetch('/api/user', { signal: controller.signal })),
controller.signal
);
// To abort:
controller.abort();Method Chaining
AsyncResult supports fluent chaining through its Thenable interface:
import { fromPromise } from '@deessejs/fp';
const result = await fromPromise(fetchUser(id))
.map(user => ({ ...user, fullName: user.name.toUpperCase() }))
.flatMap(user => fromPromise(saveUser(user)))
.tap(user => console.log('Saved:', user))
.mapErr(error => ({ ...error, timestamp: Date.now() }));Real-World Examples
Chained API Calls
import { fromPromise, ok, flatMap, isOk } from '@deessejs/fp';
interface User { id: number; name: string; }
interface Post { id: number; title: string; }
const fetchUser = (id: number) =>
fromPromise(
fetch(`/api/users/${id}`).then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json() as Promise<User>;
})
);
const fetchPosts = (userId: number) =>
fromPromise(
fetch(`/api/users/${userId}/posts`).then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json() as Promise<Post[]>;
})
);
// Chain them
const getUserWithPosts = (id: number) =>
flatMap(fetchUser(id), user =>
flatMap(fetchPosts(user.id), posts =>
ok({ user, posts })
)
);
const result = await getUserWithPosts(1);
if (isOk(result)) {
console.log(result.value.user.name, result.value.posts.length);
}Parallel Data Fetching
import { fromPromise, all, traverse } from '@deessejs/fp';
const fetchDashboard = async (userId: number) => {
const [user, stats, notifications] = await all(
fromPromise(fetch(`/api/users/${userId}`).then(r => r.json())),
fromPromise(fetch(`/api/users/${userId}/stats`).then(r => r.json())),
fromPromise(fetch(`/api/users/${userId}/notifications`).then(r => r.json()))
);
return { user, stats, notifications };
};
const processItems = async (ids: number[]) => {
const results = await traverse(ids, async id => {
const data = await fetchItem(id);
return transformItem(data);
});
return results;
};Error Recovery
import { fromPromise, ok, err, isOk } from '@deessejs/fp';
const fetchWithFallback = async (primaryUrl: string, fallbackUrl: string) => {
const primary = await fromPromise(fetch(primaryUrl));
if (isOk(primary)) {
return ok(await primary.value.json());
}
const fallback = await fromPromise(fetch(fallbackUrl));
if (isOk(fallback)) {
return ok(await fallback.value.json());
}
return err(new Error('Both primary and fallback failed'));
};AsyncResult vs Result vs Try
| Aspect | AsyncResult | Result | Try |
|---|---|---|---|
| Operations | Async | Sync | Sync |
| Error handling | Explicit errors | Explicit errors | Catches exceptions |
| API | Thenable | Direct | Direct |
| Cancellation | AbortSignal | N/A | N/A |
// AsyncResult: async operations with explicit errors
const user = await fromPromise(fetch('/api/user'));
// Result: sync operations with explicit errors
const parsed = parseUser(input); // Result<User, ParseError>
// Try: sync operations that might throw
const parsed = attempt(() => JSON.parse(input));