You’re testing a new feature in a development environment. You click “Submit,” and a few seconds later, your phone buzzes with a real-world SMS notification. Or worse, a real customer receives a “Test” email meant for a sandbox user. While these aren’t usually “delete-the-database” disasters, they represent a fundamental failure in application guardrails.
In the standard imperative programming most of us use every day, the decision to act and the act itself are fused together. When your code reaches a line like await smsProvider.send(), the trigger has already been pulled and the network request is on its way.
There is no airlock where a supervisor process can intercept a dangerous command and say, “Wait, this is a dev environment; log this to the console instead of sending it to a customer.” Because there is no space between your logic and its execution, your rules are effectively just promises rather than enforced constraints.
We can bridge this gap by writing functions that return data-driven descriptions of work instead of calling external services directly. By treating a database write or an API call as a plain JavaScript object rather than an immediate action, we can inspect, validate, or even block the intent before it ever touches the network.
In a series of articles, we’ve been exploring a tiny, home-grown Effect System that follows the “Functional Core, Imperative Shell” pattern. Instead of performing side effects, our functions describe them. Rather than executing an action, our code returns a Command, which is a plain JavaScript object that acts as a “recipe” for work to be executed later.
Here is how we can refactor the SMS sending function using our Effect System:
import { Success, Failure, Command, effectPipe, runEffect } from 'pure-effect';
function notifyUser(user, message) {
// Define the side effect, but don't run it yet
const cmdNotifyUser = () => smsProvider.send(user.phone, message);
// Define what happens with the result
const next = (sent) => sent ? Success('SMS sent') : Failure('Failed to send SMS');
// Add optional metadata to tag the actions we want to govern
const meta = {
domain: Domain.NOTIFICATIONS,
risk: Risk.MEDIUM,
action: Action.SEND_SMS,
};
return Command(cmdNotifyUser, next, meta);
}
Our Effect System really shines when composing multiple pure functions, as shown in the example below. We use an effect pipeline to pass the output of one function to the subsequent one. The Success and Failure cases are handled automatically. If a function returns Success, the subsequent function in line will be called. In the case of a Failure, the pipeline terminates.
// Functional Core: Describing the work to be done
const notifyUserFlow = (userId, message) =>
effectPipe(
getUser,
tap(isNotificationEnabled),
(user) => notifyUser(user, message)
)(userId);
// ...
app.post('/billing/notify', async (req, res) => {
const context = {
env: process.env.NODE_ENV,
user: req.user,
domain: Domain.BILLING
};
// The Imperative Shell: Performing the work
const result = await runEffect(notifyUserFlow(req.user.id, req.message), context);
// ...
});
At this point, runEffect becomes the central supervisor for every side effect in our application. Whether the intent is a simple database read (getUser) or an external API call (notifyUser), every single step must pass through it.
This allows us to move our application rules out of the README and into the execution layer, where they can be automatically applied.
We can inject a global interceptor using configureEffect. We use the onBeforeCommand hook to inspect the metadata of a command and the current context of the application before allowing any side effect to occur.
import { configureEffect } from 'pure-effect';
const appGuardrails = (command, context) => {
const { meta } = command;
if (!meta) return;
if (context.env === 'development' && meta.action === Action.SEND_SMS) {
throw new Error('Real SMS is disabled in Dev to prevent accidental spam.');
}
if (context.domain === Domain.BILLING && meta.domain === Domain.USERS) {
throw new Error("Billing logic shouldn't be modifying User records directly.");
}
if (meta.risk === Risk.HIGH && context.user.role !== Role.ADMIN) {
throw new Error('Access denied: High-risk actions require an Admin session.');
}
};
configureEffect({ onBeforeCommand: appGuardrails });
Of course, running a long list of rules before every Command carries a performance cost. However, these guardrails can be applied selectively. Many can be reserved for development and disabled in production to ensure the system remains lean.
We spend hours on diagrams and READMEs, but those rules only work if they are remembered during every single pull request. As a codebase grows, maintaining that mental checklist is a significant challenge.
Our Effect System transitions those rules from static documentation into active runtime guardrails. By creating a gap between the intent to act and the execution of that act, we move the burden of checking application boundaries to the system itself.
These guardrails are a safety net for a fast-moving environment. They provide the entire team with the confidence that even in complex scenarios or unusual configurations, the system has a built-in mechanism to validate the intended action before any side effects occur.
GitHub Repository: pure-effect
Related: