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
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