Safely Evaluating Feature Flags with JavaScript Expressions

Feature flag systems often rely on dynamic logic — "enable this for logged-in users", "disable for legacy browsers", etc. Some teams use simple booleans. Others reach for configuration platforms that support expression-based evaluation. But what if you want full control? In this post, you’ll learn how to safely evaluate feature flag expressions in JavaScript, without exposing your app to security risks or runtime failures. Step 1: Accept Expressions as Strings (Cautiously) You might store your flag definitions like this: const flagConfig = { betaDashboard: "user.role === 'beta' && user.isActive", enableGifts: "context.env === 'production' && getBucket(user.id) < 50", }; The power of expressions is flexibility: they can target users, environments, percentages, etc. Step 2: Create a Controlled Evaluation Environment To avoid security issues, don’t eval() blindly. Instead, use Function to define a scoped evaluator: function evaluateFlags(flagDefs, user, context = {}) { const results = {}; for (const [key, expr] of Object.entries(flagDefs)) { try { const fn = new Function('user', 'context', 'getBucket', `return (${expr})`); results[key] = !!fn(user, context, getBucket); } catch (err) { console.error(`Error evaluating flag "${key}":`, err); results[key] = false; } } return results; } This restricts what can be accessed inside the flag logic. You control the available functions (getBucket, etc). Step 3: Provide Helper Functions Like getBucket() If you want gradual rollouts or segmenting by percentage, include helpers like this: function getBucket(id) { let hash = 0; for (let i = 0; i < id.length; i++) { hash = (hash

May 2, 2025 - 04:13
 0
Safely Evaluating Feature Flags with JavaScript Expressions

Feature flag systems often rely on dynamic logic — "enable this for logged-in users", "disable for legacy browsers", etc. Some teams use simple booleans. Others reach for configuration platforms that support expression-based evaluation. But what if you want full control?

In this post, you’ll learn how to safely evaluate feature flag expressions in JavaScript, without exposing your app to security risks or runtime failures.

Step 1: Accept Expressions as Strings (Cautiously)

You might store your flag definitions like this:


const flagConfig = {
  betaDashboard: "user.role === 'beta' && user.isActive",
  enableGifts: "context.env === 'production' && getBucket(user.id) < 50",
};

The power of expressions is flexibility: they can target users, environments, percentages, etc.

Step 2: Create a Controlled Evaluation Environment

To avoid security issues, don’t eval() blindly. Instead, use Function to define a scoped evaluator:


function evaluateFlags(flagDefs, user, context = {}) {
  const results = {};

  for (const [key, expr] of Object.entries(flagDefs)) {
    try {
      const fn = new Function('user', 'context', 'getBucket', `return (${expr})`);
      results[key] = !!fn(user, context, getBucket);
    } catch (err) {
      console.error(`Error evaluating flag "${key}":`, err);
      results[key] = false;
    }
  }

  return results;
}

This restricts what can be accessed inside the flag logic. You control the available functions (getBucket, etc).

Step 3: Provide Helper Functions Like getBucket()

If you want gradual rollouts or segmenting by percentage, include helpers like this:


function getBucket(id) {
  let hash = 0;
  for (let i = 0; i < id.length; i++) {
    hash = (hash << 5) - hash + id.charCodeAt(i);
    hash |= 0;
  }
  return Math.abs(hash % 100); // 0–99
}

This allows expressions like:


"getBucket(user.email) < 20"

...to target a consistent 20% of users.

Step 4: Validate or Restrict Expressions (Optional)

For added safety, consider linting or validating expressions at build time. You can:

  • Disallow certain keywords (window, document)
  • Limit expression length
  • Require static analysis before deployment

This helps catch dangerous or broken logic before it ships.

Step 5: Pre-Evaluate and Persist Flags

To avoid re-evaluating expressions every render, evaluate once and cache the result:


async function initFeatureFlags(user, context) {
  const flags = evaluateFlags(flagConfig, user, context);
  localStorage.setItem('evaluatedFlags', JSON.stringify(flags));
  return flags;
}

Later, just read from localStorage or context provider.

Pros:

  • ✨ Supports expressive, dynamic logic per user/session