Every time you write "if we're on the browser, do X; if we're on the server, do Y", you have made a runtime assumption. They seem harmless. One conditional in one function, for a good reason, on a Tuesday when you were trying to ship. They compound.
Give it a year. That codebase has dozens of them. Each one made sense individually. Together they form something you cannot test, cannot move, and cannot explain to a new team member without walking through every environment your code runs in.
What runtime assumptions look like in practice
They are not always obvious. Some of the most common patterns:
typeof window !== 'undefined'scattered through utility functionsprocess.env.NODE_ENV === 'browser'buried inside business logicif (isServer) { log to file } else { log to console }mixed into domain code- Functions that call
localStoragedirectly instead of taking a storage interface - Date formatting that assumes the server timezone is UTC
- Error handling that assumes a network stack is available
Each of these is a runtime assumption baked into logic that should be environment-agnostic. The logic says: "I know where I am running, and I behave differently based on that knowledge."
Why they accumulate
Each assumption is locally rational. You need to log somewhere. You need to read config from somewhere. You need to format dates for the user's locale. The environment provides these things differently depending on context, and the path of least resistance is an inline check.
The alternative would be abstracting behind an interface, injecting dependencies, or designing the logic to receive context rather than detect it. That takes more time on any individual PR. So teams take the shortcut, mark the code with a comment saying "clean this up later", and move on.
Later rarely comes. The assumption hardens. Other code starts relying on the same pattern. The shortcut becomes the standard.
The hidden cost
The immediate cost of a runtime assumption is small. Almost zero. The deferred cost is what gets you.
Testing becomes environment-specific. You cannot unit test logic that checks typeof window without either mocking the browser environment or running the tests in a browser. Your test suite forks. You end up with browser tests and server tests for the same logical behavior, and keeping them synchronized is manual work.
Behavior becomes unpredictable across environments. As the assumptions accumulate, the function's behavior in a new environment depends on which assumptions trigger. Add an edge runtime and now you need to audit every assumption to understand what "edge" evaluates to. Some assumptions will evaluate to neither branch. Some will throw. You discover this in production.
New environments mean new code paths. Adding AI pipeline support or an edge worker should not require touching business logic. But if the business logic is full of environment checks, every new environment requires a new code path review. You cannot just drop the capability somewhere new and trust it will behave correctly.
The contracts-first alternative
The contracts-first approach removes environment detection from business logic entirely.
A capability declares what it needs from the environment, not what environment it is in. It declares: "I need a clock, a logger, and access to the orders table." The runtime reads this from the contract and provides the correct implementations for wherever the capability is running. The capability itself never checks.
Placement is a deployment decision, not a logic decision. The capability does not know where it runs. The runtime does. The runtime was designed to know. The capability was designed to compute correctly regardless.
This is not a new idea. It is dependency injection applied at the runtime level, enforced by a contract, with the placement logic moved to infrastructure where it belongs.
One concrete example refactored
Here is a discount eligibility check with a common runtime assumption baked in:
The before version works fine as long as you only run it in the two environments it knows about. The after version works in any environment because it does not know about any of them. The contract tells the runtime to inject a resolved Tier value. The runtime knows how to get that value in each context. The logic does not care.
This is a small change. Applied consistently, across a whole codebase, it is the difference between logic that can run anywhere and logic that can only run where it already runs.
The next time you reach for an environment check inside business logic, it is worth pausing for a moment. The cost right now is near zero. The cost in eighteen months, when someone asks where else this can run, is much harder to calculate.