Aikido

How to detect and fix contradictory logic in your code

Logic bug

Rule
Detect contradictory or impossible logic
Code that checks conditions after they've 
already been violated, or assumes states
that are impossible given the control flow.

Supported languages: 45+

Introduction

Contradictory logic appears when code checks conditions that are already known to be true or false based on earlier control flow. This happens after refactoring when validation gets reordered, or when developers add defensive checks without understanding what guarantees already exist. A function that checks if (user !== null) after calling user.email has contradictory logic, the null check comes too late. These logical impossibilities indicate deeper problems with code organization or missing understanding of what each code path guarantees.

Why it matters

Security implications: False validation creates a dangerous illusion of safety. When security checks appear after the data has already been used, attackers can exploit the window before validation occurs. Code that validates user permissions after executing privileged operations provides no actual protection, just misleading comments about security.

Code maintainability: Contradictory logic suggests the code doesn't match the developer's mental model. Someone thought a condition needed checking but placed it wrong, or the code was refactored without updating related checks. Future maintainers can't trust that validation exists where needed, forcing them to trace through entire functions to understand actual guarantees.

Bug indicators: Impossible conditions rarely exist in isolation. They signal deeper issues like missing error handling, incorrect assumptions about function contracts, or failed refactoring. A check that can never execute often means a different check is missing somewhere else that should have prevented this state.

Code examples

❌ Non-compliant:

function processOrder(order) {
    if (!order) {
        return { error: 'Order required' };
    }

    const total = order.items.reduce(
        (sum, item) => sum + item.price,
        0
    );

    if (order.items && order.items.length > 0) {
        applyDiscount(order);
    }

    if (total < 0) {
        throw new Error('Invalid total');
    }

    return { total, status: 'processed' };
}

Why it's wrong: The code calls order.items.reduce() which crashes if items is null or undefined, then checks if items exists afterward. The total < 0 check is also contradictory because the reduce always returns non-negative values when summing prices.

✅ Compliant:

function processOrder(order) {
    if (!order || !order.items || order.items.length === 0) {
        return { error: 'Valid order with items required' };
    }

    const hasInvalidPrice = order.items.some(
        item => typeof item.price !== 'number' || item.price < 0
    );

    if (hasInvalidPrice) {
        throw new Error('Invalid item prices');
    }

    const total = order.items.reduce(
        (sum, item) => sum + item.price,
        0
    );

    if (order.items.length >= 5) {
        applyBulkDiscount(order);
    }

    return { total, status: 'processed' };
}

Why this matters: All validation occurs before using the data, checks happen in logical order, and conditions reflect actual requirements. The function validates inputs upfront, then processes valid data without redundant or contradictory checks.

Conclusion

Place validation before using data, not after. Review conditions that seem defensive but appear after the data has already been accessed or modified. When refactoring, update or remove related validation to maintain logical consistency throughout the function.

FAQs

Got Questions?

How do I distinguish defensive programming from contradictory logic?

Defensive programming validates inputs at function boundaries before use. Contradictory logic validates after use or checks conditions already guaranteed by earlier code. The timing and necessity matter. If user was already dereferenced with user.email, then checking if (user) afterward is contradictory, not defensive.

What about type narrowing in TypeScript?

TypeScript's control flow analysis tracks what's possible at each point. If TypeScript allows a check, the condition might be reachable. But runtime JavaScript doesn't enforce these types, so contradictory runtime checks can still exist despite type safety. Focus on runtime control flow, not just static types.

Can contradictory logic cause security vulnerabilities?

Yes, when security checks occur after privileged operations execute. Checking permissions after database writes, validating input after SQL query execution, or verifying authentication after exposing sensitive data all create time-of-check-time-of-use vulnerabilities. Validation must precede action for security to be effective.

What about error handling code that seems impossible?

Error handlers for exceptions that can't occur given earlier validation might indicate over-defensive programming or outdated error handling from before validation was added. Review whether each error path is actually reachable. If impossible, remove it to simplify the code and avoid misleading future maintainers.

How do I find contradictory logic in existing code?

Look for validation checks after data access, conditions that are always true or false given earlier branches, and error handling for states already prevented. Code coverage tools help identify unreachable branches. Manual review of validation patterns reveals checks that happen too late or test impossible conditions.

Get secure for free

Secure your code, cloud, and runtime in one central system.
Find and fix vulnerabilities fast automatically.

No credit card required | Scan results in 32secs.