withTryCatch
Structured try / catch / finally blocks for flow steps. Wraps one or more steps in an exception-safe block without reaching for top-level error handlers.
Setup
import { FlowBuilder } from "flowneer";
import { withTryCatch } from "flowneer/plugins/resilience";
const AppFlow = FlowBuilder.extend([withTryCatch]);Usage
import { FlowBuilder, fragment } from "flowneer";
import { withTryCatch } from "flowneer/plugins/resilience";
const AppFlow = FlowBuilder.extend([withTryCatch]);
const flow = new AppFlow<State>()
.try(fragment<State>().then(fetchData).then(processData))
.catch(
fragment<State>().then((s) => {
console.error("Pipeline failed:", s.__tryError);
s.result = "fallback";
}),
)
.finally(fragment<State>().then(cleanup))
.then(sendResult);API
.try(fragment)
Executes all steps in fragment. If any step throws, control passes to the .catch() fragment (if registered), or the error propagates.
.catch(fragment)
Handles an error thrown inside the preceding .try(). The error is available on shared.__tryError before the fragment runs and is removed once the fragment completes.
If the catch fragment also throws, the error propagates (and the .finally() fragment still runs).
.finally(fragment)
Always runs after the .try() (and optional .catch()), regardless of success or failure. Calling .finally() closes the try/catch block.
Note:
.catch()and.finally()must be called immediately after.try()— no other.then()or builder calls can appear between them.
InterruptError:
InterruptError(thrown bywithHumanNodeand flow abort signals) is never caught by.try()/.catch()— it propagates immediately to the caller just like it would outside a try block.
__tryError context
The caught error is stored on shared.__tryError inside the catch fragment:
.catch(
fragment<State>().then((s) => {
const err = s.__tryError; // original Error or value that was thrown
if (err instanceof Error) {
s.errorMessage = err.message;
}
s.usedFallback = true;
}),
)__tryError is always the original thrown value. If Flowneer wrapped it in a FlowError, the unwrapped cause is exposed here.
Nested blocks
Try/catch blocks can be nested:
const AppFlow = FlowBuilder.extend([withTryCatch]);
const flow = new AppFlow<State>()
.try(
fragment<State>()
.try(fragment<State>().then(riskyInner))
.catch(
fragment<State>().then((s) => {
s.innerFailed = true;
}),
)
.then(continueFrag),
)
.catch(
fragment<State>().then((s) => {
s.outerFailed = true;
}),
);Example — fetch with recovery
import { FlowBuilder, fragment } from "flowneer";
import { withTryCatch } from "flowneer/plugins/resilience";
const AppFlow = FlowBuilder.extend([withTryCatch]);
interface State {
userId: string;
profile: Record<string, unknown> | null;
fromCache: boolean;
}
const flow = new AppFlow<State>()
.try(
fragment<State>().then(async (s) => {
const res = await fetch(`/api/users/${s.userId}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
s.profile = await res.json();
}),
)
.catch(
fragment<State>().then(async (s) => {
console.warn("Live fetch failed, loading from cache:", s.__tryError);
s.profile = await loadFromCache(s.userId);
s.fromCache = true;
}),
)
.then((s) => {
console.log("Profile ready, fromCache:", s.fromCache);
});