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