Skip to content

Guards

Guards are the most powerful unit in Pulse.js. They serve as reactive “firewalls” that can compute derived state, handle asynchronous logic, and prevent inconsistent updates to your UI.

In Pulse v3, Guards have been enhanced to act as high-performance Selectors with built-in dependency tracking and rich failure context.

You can now use Guards as lightweight computed values. When you access other reactive units inside the guard function, Pulse automatically subscribes to them.

import { guard, pulse } from '@pulse-js/core';
const user = pulse({ name: 'Alice', role: 'guest' });
const isAdmin = guard('is-admin', () => {
return user.role === 'admin';
});

Guards keep track of their own status (ok, fail, pending) and an explicit failure reason. Unlike simple computed values, they provide semantic context for why a calculation failed or is still loading.

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

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 v3, this is guaranteed to be a structured object, allowing you to access detailed error information.

const reason = isAuthorized.reason();
if (reason) {
console.log(reason.code); // e.g. "AUTH_REQUIRED"
console.log(reason.message); // e.g. "You must be logged in"
}

Pulse reasons implement toString(), so you can still render them directly in your UI (e.g. {isAuthorized.reason()}) if you just want to show the message.

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: { code: 'BALANCE', message: 'Insufficient balance' },
dependencies: [
{ name: 'has-items', status: 'ok' },
{
name: 'sufficient-balance',
status: 'fail',
reason: { code: 'LOW_FUNDS', message: '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.