In traditional software development, the focus is often on the so-called happy path: the positive flow where valid input is processed and the expected result is returned. But it is just as important to consider which states should never be possible in the first place.

Just as the empty space around an object helps define its shape, we can think about code in terms of what must not happen. This is the idea behind Negative Space Programming.

When defensive code hides broken states

Defensive code is not a problem in itself. The problem arises when a broken internal contract is treated as a normal outcome, allowing the underlying issue to remain hidden in the system.

Consider the following example of a function for calculating a total. In this case, the contract states that the discount engine must never generate a discount greater than the subtotal:

Returning 0 to prevent a negative balance may look safe at first glance, but it hides the fact that the discount engine has generated an impossible discount. The code appears safe, but the bug is still there.

It also becomes harder to find because the actual rule is never expressed in the code. Anyone reviewing or debugging it has to work out for themselves which assumption the code relies on.

Assertions or error handling?

To make hidden assumptions executable, assertions can be used directly in the application logic, not only in unit tests. It is important to distinguish between expected errors and broken internal contracts:

  • Error handling (if statements, try/catch): Used when an error path is an expected part of the business logic, such as when a user enters an invalid discount code.
  • Assertions: Used for internal promises and invariants that must always hold true, for example that every order in the shipping queue has the payment status Captured.

The key difference is what the state represents. A declined or not-yet-completed payment is an expected outcome that should be handled as part of the normal control flow. An unpaid order in a queue that should only contain paid orders, on the other hand, represents a broken internal contract.

An assertion declares a condition that absolutely must be true. If that condition turns out to be false, execution stops immediately, close to the source of the error.

Put the rule where it belongs

In the demo during Softhouse Learning Lunch, I used a simple checkout flow and a shipping queue. The queue had an implicit contract: everything in it must be paid for.

The first implementation was defensive. A nightly job checked each order, logged a warning and skipped anything that had not been paid. The job could therefore complete without crashing, but the underlying problem was still there.

When the check was expressed as an assertion instead, it became clear that it was also happening too late in the flow. The rule did not belong in the nightly job. It belonged at the boundary of the queue, when an order attempts to enter it:

Once the assertion was moved to enqueue, the error was detected immediately when the contract was broken. The stack trace led back to the checkout flow, where the actual problem was found: the != Failed condition allowed not only paid orders through, but also orders with the status Authorized, meaning payments where the funds had not yet been captured.

The solution was therefore not to add more assertions. Instead, the checkout flow was changed to handle the expected payment statuses as normal control logic:

Authorized and Failed are expected states in the checkout flow. The broken internal contract only occurs if an order that is not Captured attempts to enter the shipping queue.

The point, then, is not to replace every if statement with an assertion. It is to put the right rule at the boundary that owns it.

Where should the rule live?

More assertions do not automatically mean better code. The point is to place the right rule in the right place, where the invalid state first becomes possible. I usually ask three questions:

  1. Can the rule be expressed as a type? If possible, use a type whose construction guarantees the properties the rest of the code depends on. An Email type, for example, can validate its value when it is created, so that the rest of the code does not need to repeat the same checks.
  2. Is this an expected error? Handle it as a normal error in the application flow.
  3. Is this an internal system promise? Add an assertion that validates the condition directly in the application logic.

Assertions in production

Using assertions in production does not mean that the goal is to bring down the entire application. The goal is to make a broken internal contract visible close to its source and prevent the system from silently continuing in a state that, by design, should never be possible.

How such a failure is contained depends on the language, runtime environment and system architecture. In a web service, for example, it may be possible to terminate a single request or transaction while allowing the rest of the service to continue.

Context is crucial. In safety-critical or embedded systems, simply crashing and restarting may itself be dangerous. In those environments, failure handling needs to be designed around the system’s specific safety and recovery requirements.

Assertions complement tests. They do not replace them.

What does this give us in practice?

Making the rules explicit in the code brings several concrete benefits:

  • Safer code: Well-placed rules detect broken contracts earlier and reduce the risk of cascading failures.
  • Clearer code reviews: Assertions act as executable documentation, making the developer’s exact assumptions visible.
  • Faster debugging: Clear failures and stack traces show where the contract was broken, making it easier to trace the root cause.
  • Better design: Vague “just in case” branches can be eliminated and replaced with cleaner interface contracts.

Developer checklist

  1. Identify a hidden assumption in the codebase, such as a list that must never be empty.
  2. Evaluate the mechanism: Use types to eliminate invalid states, error handling for expected failures, and assertions for internal system promises.
  3. Make the rules visible, and make impossible states fail loudly.

Knowledge hub

Your gateway to insights, guides, and expert content on everything from AI and software development to digital transformation. Explore blogs, articles, reports, and resources designed to help both decision-makers and developers stay ahead in the tech landscape.

Softhouse content

Share This!

By Published On: 2026-09-30Categories: Articles, Coding, engineeringComments Off on Technical Guide: Negative Space Programming in Practice