If your feature flag system goes down, do your users see a blank screen? Many devs rely on remote config services, but don’t build for failure. In this post, you’ll learn how to design a fail-safe, offline-capable feature flag layer in JavaScript using:
- Static flag snapshots
- Local evaluation of flag expressions
- Resilient bootstrapping with fallbacks
This approach ensures your app always knows what to show, even when remote config APIs or CDNs are down.
Step 1: Define a Static Local Fallback
Start with a minimal embedded JSON fallback:
const fallbackFlags = {
darkMode: "user.preferences.includes('dark')",
newSearch: "false", // force disabled
};
This is hardcoded at build time — not ideal for dynamic control, but perfect for guaranteed rendering.
Step 2: Attempt to Load Remote Flags, But Don’t Block
At runtime, try to fetch the latest flags asynchronously, but fall back immediately if needed:
async function loadFlags() {
try {
const res = await fetch('/config/flags.json', { cache: 'reload' });
if (!res.ok) throw new Error('Bad response');
return await res.json();
} catch (e) {
console.warn('Using fallback flags', e);
return fallbackFlags;
}
}
This means your UI never waits on a remote response to start rendering.
Step 3: Evaluate Flags Using JavaScript Expressions
Use a safe evaluator to run each flag expression:
function evaluateFlags(flags, user, contextFns = {}) {
const results = {};
for (const [key, expr] of Object.entries(flags)) {
try {
const fn = new Function('user', ...Object.keys(contextFns), `return (${expr})`);
results[key] = !!fn(user, ...Object.values(contextFns));
} catch {
results[key] = false;
}
}
return results;
}
Supports fallbacks, remote flags, and even gradual rollout helpers (e.g. getBucket()
).
Step 4: Persist the Last Good Config Locally
Save flags to localStorage
or IndexedDB to survive reloads:
async function initFlags(user) {
let flags = fallbackFlags;
try {
const remote = await loadFlags();
localStorage.setItem('lastFlags', JSON.stringify(remote));
flags = remote;
} catch {
const cached = localStorage.getItem('lastFlags');
if (cached) flags = JSON.parse(cached);
}
return evaluateFlags(flags, user, { getBucket });
}
Now your app is resilient to CDN failures, cold starts, and network drops.
Step 5: Use Pre-Evaluated Flags in React/SPA Frameworks
Once evaluated, expose flags globally:
const FeatureFlagsContext = createContext({});
export function FlagsProvider({ children, user }) {
const [flags, setFlags] = useState({});
useEffect(() => {
initFlags(user).then(setFlags);
}, [user]);
return (
{children}
);
}
This means all flags are evaluated once per session, and ready across components instantly.
✅ Pros:
- 🛡️ No single point of failure — flags work offline
- ⚡ Instant startup, even on slow networks
- 🧪 Easy to test with static mocks
- 📦 Can ship flag snapshots in Docker, offline apps, or airgapped systems
⚠️ Cons:
- 🔄 No live updates without reloading or polling
- ❌ Stale flags can persist if localStorage isn’t cleared
- 🔍 Requires careful flag naming/versioning to avoid conflicts
Summary
Most devs don’t think about what happens when feature flag APIs fail. This article showed how to build offline-first, fail-safe flag evaluation using local fallback JSON and expression-based evaluation. With just a few lines of code, your app can gracefully degrade without ever relying on a third-party flag service.
Want to go deeper? My full guide explains in detail how to:
- Use JavaScript expressions for safe feature flag evaluation
- Handle gradual feature rollouts and exposure
- Implement flag versioning, migration strategies, and more
- Design a feature flagging system that works offline and is resilient to failure
Feature Flag Engineering Like a Pro: From JS Expressions to Global Rollouts — just $10 on Gumroad.
If this was helpful, you can also support me here: Buy Me a Coffee ☕