Skip to content

Callback Flattening & Guards

The use keyword flattens nested callbacks into linear code. Combined with guard functions, it gives you early returns without if, return, or nesting.

use takes the rest of the block and passes it as a callback to the function on the right:

use file <- File.open(path)
use contents <- File.readAll(file)
contents |> String.toUpper

This compiles to:

File.open(path, (file) => {
File.readAll(file, (contents) => {
return String.toUpper(contents);
});
});

use works with any function whose last parameter is a callback. No special trait or interface required.

When the callback takes no arguments, omit the binding:

use <- Timer.delay(1000)
Console.log("done")

Compiles to:

Timer.delay(1000, () => {
console.log("done");
});

Guards are the killer use case for use. They give you React-style early returns in a flat, linear flow — no nesting, no if, no return keyword.

In TypeScript, components with preconditions look like this:

function AdminPage({ auth, user, data }) {
if (!auth.isAdmin) return <Forbidden />;
if (!auth.isVerified) return <VerifyPrompt />;
if (!user) return <LoginPage />;
if (data.error) return <ErrorPage error={data.error} />;
return <Dashboard user={user} data={data.value} />;
}

This works, but it’s all imperative control flow with early returns.

type AdminPageProps {
auth: Auth,
maybeUser: Option<User>,
data: Result<Data, AppError>,
}
export fn AdminPage(props: AdminPageProps) -> JSX.Element {
use <- Bool.guard(props.auth.isAdmin, <Forbidden />)
use <- Bool.guard(props.auth.isVerified, <VerifyPrompt />)
use user <- Option.guard(props.maybeUser, <LoginPage />)
use data <- Result.guard(props.data, (e) => <ErrorPage error={e} />)
// by here: admin, verified, user unwrapped, data unwrapped
<Dashboard user={user} data={data} />
}

Same linear flow, but each guard also narrows the type:

  • Bool.guard — continues if the condition is true, bails with fallback if false
  • Option.guard — unwraps Some(value), bails with fallback on None
  • Result.guard — unwraps Ok(value), bails with error handler on Err

Continue if true, bail with a fallback value if false:

use <- Bool.guard(condition, fallbackValue)
// only runs if condition is true
type PremiumContentProps { isPaid: boolean }
export fn PremiumContent(props: PremiumContentProps) -> JSX.Element {
use <- Bool.guard(props.isPaid, <UpgradePage />)
<PremiumDashboard />
}

Unwrap Some, bail on None:

use value <- Option.guard(optionValue, fallbackValue)
// value is unwrapped here
type ProfileProps { maybeUser: Option<User> }
export fn Profile(props: ProfileProps) -> JSX.Element {
use user <- Option.guard(props.maybeUser, <LoginPrompt />)
<ProfileCard name={user.name} />
}

Unwrap Ok, bail on Err with an error handler:

use value <- Result.guard(resultValue, (err) => fallbackValue)
// value is the Ok value here
type DataPageProps { result: Result<Data, ApiError> }
export fn DataPage(props: DataPageProps) -> JSX.Element {
use data <- Result.guard(props.result, (e) => <ErrorBanner error={e} />)
<DataTable rows={data.rows} />
}

Guards compose naturally. Each one narrows the type for everything below it:

type OrderPageProps {
auth: Auth,
maybeOrder: Option<Order>,
paymentResult: Result<Payment, PaymentError>,
}
export fn OrderPage(props: OrderPageProps) -> JSX.Element {
use <- Bool.guard(props.auth.isLoggedIn, <LoginPage />)
use order <- Option.guard(props.maybeOrder, <p>Order not found</p>)
use payment <- Result.guard(props.paymentResult, (e) =>
<PaymentError message={e.message} />
)
<OrderConfirmation order={order} payment={payment} />
}

Guards are just stdlib functions — no new syntax. Bool.guard has this signature:

Bool.guard(condition: boolean, fallback: T, continuation: () -> T) -> T

When you write use <- Bool.guard(cond, fallback), the use keyword takes everything after that line and passes it as the continuation callback. If cond is false, fallback is returned without calling the continuation.

The same pattern works for Option.guard and Result.guard — they just unwrap the value and pass it to the continuation.

Both handle early exits, but for different situations:

?use + guard
Works withResult and OptionAny type (booleans, Options, Results)
ReturnsErr / None to the callerAny fallback value
Best forPropagating errors up the call chainRendering different UI for different states
RequiresFunction returns Result or OptionNothing — works in any function

Use ? when you want to bubble errors up. Use guards when you want to handle conditions inline with a specific fallback.