Skip to content

Guards

Guards are the heart of Pulse. They represent Conditioned Truths or business rules. Unlike simple computed values, Guards track their own status (ok, fail, pending) and explicit failure reason.

In traditional state management, you often handle “loading”, “error”, and “success” states manually with separate variables.

// The old way
let isLoading = false;
let error = null;
let data = null;

A Guard encapsulates all of this.

import { guard } from '@pulse-js/core';
const isAuthorized = guard('auth-check', async () => {
const user = currentUser();
if (!user) throw 'Not logged in';
if (user.role !== 'admin') return false; // Fail
return true; // OK
});
  1. OK: The evaluator returned a truthy value (or true).
  2. FAIL: The evaluator returned false or threw an error.
  3. PENDING: The evaluator returned a Promise that is currently resolving, or returned undefined.

Returns true if the last evaluation was successful.

Returns true if the last evaluation failed.

Returns true if the guard is currently evaluating (async).

Returns the reason for the failure. In Pulse v0.2.2, this is always a structured object.

if (isAuthorized.fail()) {
console.error(isAuthorized.reason()); // "Not logged in"
}

Sometimes returning false isn’t enough. You might want to fail with a specific error code or return early.

Import guardFail to stop execution immediately and mark the guard as failed.

Structured Reasons (Async Friendly): You can pass a structured object to provide more context to your UI.

import { guard, guardFail } from '@pulse-js/core';
const userCheck = guard('user-auth', async () => {
const user = await fetchUser();
if (!user) {
guardFail({
code: 'AUTH_REQUIRED',
message: 'User must be logged in',
meta: { redirect: '/login' }
});
}
return true;
});

Return early with a success value.

import { guard, guardOk } from '@pulse-js/core';
const check = guard(() => {
if (cache.has(key)) return guardOk(cache.get(key));
// ... expensive logic
return result;
});

Pulse provides powerful ways to compose guards together.

Transforms a Source into a Guard. This is the recommended way to derive business logic from state.

import { source, guard } from '@pulse-js/core';
const todos = source([{ done: false }, { done: true }]);
// Reactive guard derived from source
const doneCount = guard.map(todos, list =>
list.filter(t => t.done).length
);
  • guard.all(name, [guards]): Succeeds if all guards succeed.
  • guard.any(name, [guards]): Succeeds if at least one guard succeeds.
  • guard.not(name, guard): Inverts the status of a guard.

Returns a complete snapshot of the guard’s state and its recursive dependency tree.

[!NOTE] For failed guards, the dependency tree now includes the specific reason why each sub-dependency failed, making debugging much easier.

const explanation = canPlaceOrder.explain();
console.log(explanation);
/* Output:
{
name: 'can-place-order',
status: 'fail',
reason: 'Insufficient balance',
dependencies: [
{ name: 'has-items', status: 'ok' },
{
name: 'sufficient-balance',
status: 'fail',
reason: 'Balance is too low'
}
]
}
*/

Extract the result type from a Guard instance for better TypeScript integration.

import { guard, type InferGuardType } from '@pulse-js/core';
const userGuard = guard('user', async () => ({ name: 'Alice' }));
type User = InferGuardType<typeof userGuard>; // { name: string }

Returns the raw internal state object ({ status, value, reason, lastReason }).

Manually forces the guard to re-run its evaluator function.