Fail-Safe Feature Flags: Offline-First Evaluation with Fallbacks in JavaScript

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:

May 2, 2025 - 03:40
 0
Fail-Safe Feature Flags: Offline-First Evaluation with Fallbacks in JavaScript

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: