Loading content...
Assertions and validation are frequently confused, sometimes used interchangeably, and often applied incorrectly. This confusion leads to systems that are either brittle (assertions everywhere) or insecure (validation missing where it matters). Understanding the fundamental distinction between these concepts is essential for building robust software.
Validation checks external input—data that enters your system from users, APIs, files, or other systems. Validation failures are expected and represent normal error conditions that must be handled gracefully.
Assertions verify internal invariants—conditions that should always be true if your program logic is correct. Assertion failures indicate bugs in your code, not invalid input.
Never use assertions for validation. Assertions can be disabled in production, leaving your system unprotected. Never use validation for internal invariants—it adds unnecessary overhead and obscures programmer errors. Each tool has its domain; mixing them creates fragile, confusing code.
Validation is the process of checking whether external input conforms to expected constraints. Key characteristics of validation:
Invalid input is expected. Users make typos. APIs receive malformed requests. Files get corrupted. Validation failures are normal operations, not exceptional circumstances.
Validation must always run. In development, testing, staging, and production—validation code always executes. It cannot be compiled out or disabled.
Validation produces user-facing errors. When validation fails, the system must communicate what went wrong in terms the caller can understand and act upon.
Validation is part of the contract. Your API promises to validate input and provide meaningful errors. This is behavior clients depend on.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// VALIDATION: Checking external inputclass UserRegistrationService { async register(request: RegistrationRequest): Promise<Result<User, RegistrationError>> { // These are validations - external input, expected to fail if (!isValidEmail(request.email)) { return Result.failure(RegistrationError.invalidEmail(request.email)); } if (request.password.length < 8) { return Result.failure(RegistrationError.passwordTooShort()); } if (await this.userRepo.existsByEmail(request.email)) { return Result.failure(RegistrationError.emailAlreadyExists(request.email)); } // Validation passed - proceed with registration const user = await this.createUser(request); return Result.success(user); }} // Validation in Value Objects - enforcing domain constraintsclass EmailAddress { private constructor(private readonly value: string) {} static create(value: string): Result<EmailAddress, ValidationError> { // Validation: external input must conform to email format if (!value || value.trim().length === 0) { return Result.failure(ValidationError.required('email')); } if (!EMAIL_REGEX.test(value)) { return Result.failure(ValidationError.invalidFormat('email', value)); } if (value.length > 254) { return Result.failure(ValidationError.tooLong('email', 254)); } return Result.success(new EmailAddress(value.toLowerCase())); } toString(): string { return this.value; }}Assertions verify internal invariants—conditions that must be true if the program is correct. Key characteristics:
Assertion failures indicate bugs. If an assertion fails, there's a defect in your code or its assumptions. This is never expected in a correctly functioning system.
Assertions may be disabled. In many languages, assertions can be compiled out for production performance. Your program must be correct without them.
Assertions serve as executable documentation. They communicate programmer intent and invariants to future maintainers.
Assertions fail loudly. When an assertion fails, the program should halt or throw an unrecoverable error. Continuing with violated invariants leads to undefined behavior.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
// ASSERTIONS: Verifying internal invariantsclass BinarySearchTree<T> { insert(value: T): void { this.root = this.insertNode(this.root, value); // Assertion: BST property must hold after insertion // If this fails, there's a bug in insertNode() console.assert(this.isBinarySearchTree(), 'BST invariant violated after insert'); } private insertNode(node: TreeNode<T> | null, value: T): TreeNode<T> { if (node === null) { return new TreeNode(value); } // Assertion: node should never be undefined at this point // Our algorithm guarantees this console.assert(node !== undefined, 'Internal error: node became undefined'); if (this.comparator(value, node.value) < 0) { node.left = this.insertNode(node.left, value); } else { node.right = this.insertNode(node.right, value); } return node; }} // Assertion in state machine - enforcing valid transitionsclass OrderStateMachine { private state: OrderState = OrderState.Created; ship(): void { // Assertion: This method should only be called when state allows shipping // The caller is responsible for checking canShip() first console.assert( this.state === OrderState.Confirmed, `Bug: ship() called in invalid state: ${this.state}` ); this.state = OrderState.Shipped; this.emitEvent(new OrderShippedEvent(this.orderId)); } // Public method for callers to check before calling ship() canShip(): boolean { return this.state === OrderState.Confirmed; }} // Custom assertion function with rich diagnosticsfunction assert(condition: boolean, message: string, context?: object): asserts condition { if (!condition) { const error = new AssertionError(message); if (context) { console.error('Assertion context:', context); } console.error(error.stack); throw error; }} // Usagefunction divideArrayIntoChunks<T>(array: T[], chunkSize: number): T[][] { // Assertion: chunk size comes from our algorithm, not user input assert(chunkSize > 0, 'chunkSize must be positive', { chunkSize, arrayLength: array.length }); const chunks: T[][] = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } // Post-condition assertion: verify our algorithm produced correct output assert( chunks.flat().length === array.length, 'Chunking lost elements', { original: array.length, chunked: chunks.flat().length } ); return chunks;}When deciding between assertion and validation, ask these questions:
| Question | Validation | Assertion |
|---|---|---|
| Where does the data come from? | External sources (users, APIs, files) | Internal computation, trusted code |
| Is failure expected? | Yes, regularly | No, indicates a bug |
| What action on failure? | Graceful error response | Halt or crash |
| Who is responsible for correct data? | The caller/external system | Your own code |
| Can it be disabled? | Never | Potentially in production |
| What does failure message target? | User/API client | Developer debugging |
Misusing assertions and validation creates subtle but dangerous problems.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// ❌ ANTIPATTERN 1: Using assertions for validation// DANGEROUS: Assertions may be disabled in production!function processPayment(amount: number, cardNumber: string): void { // If assertions are disabled, this check disappears console.assert(amount > 0, 'Amount must be positive'); console.assert(cardNumber.length === 16, 'Invalid card number'); // Attacker could submit amount: -1000 (credit instead of charge) chargeCard(cardNumber, amount);} // ✅ CORRECT: Use validation for external inputfunction processPaymentSafe(amount: number, cardNumber: string): Result<Payment, PaymentError> { if (amount <= 0) { return Result.failure(PaymentError.invalidAmount(amount)); } if (!isValidCardNumber(cardNumber)) { return Result.failure(PaymentError.invalidCardNumber()); } return chargeCard(cardNumber, amount);} // ❌ ANTIPATTERN 2: Using validation for internal invariants// WASTEFUL: Adds overhead to check conditions that should always be trueclass Stack<T> { private items: T[] = []; pop(): T | null { // Validation where assertion belongs - size() is internal state if (this.items.length === 0) { // This validation runs every time, even when we know stack isn't empty return null; } return this.items.pop()!; }} // ✅ CORRECT: API provides checking, implementation uses assertionsclass StackCorrect<T> { private items: T[] = []; isEmpty(): boolean { return this.items.length === 0; } pop(): T { // Caller is responsible for checking isEmpty() first // Assertion catches bugs where callers forget to check console.assert(!this.isEmpty(), 'pop() called on empty stack'); return this.items.pop()!; } // Or provide safe alternative that uses validation tryPop(): T | null { if (this.isEmpty()) return null; return this.items.pop()!; }} // ❌ ANTIPATTERN 3: Silent assertion (assertion with side effects or recovery)function badAssertion(data: Data): void { if (!isValid(data)) { console.log('Warning: invalid data, using default'); data = getDefaultData(); // Silently "recovering" from invariant violation } process(data);} // ✅ CORRECT: Assertions should fail loudlyfunction correctAssertion(data: Data): void { assert(isValid(data), 'Internal error: received invalid data'); process(data);}Using assertions for input validation is a security vulnerability. When assertions are disabled in production (common for performance), all those 'checks' vanish. Attackers can then submit malicious input that bypasses your non-existent validation.
Whether to keep assertions enabled in production is a nuanced decision. There are two schools of thought:
Disable in Production: Traditional approach. Assertions are development aids, removed for production performance. This assumes thorough testing catches all bugs. Risk: bugs slip through, assertions can't catch them in production.
Keep Enabled (Fail-Fast): Modern approach. Assertion failures in production are better than undefined behavior from violated invariants. A crash with clear diagnostics beats silent data corruption. This aligns with the fail-fast principle we'll explore next.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
// Production-safe assertion that never gets disabled// but has configurable behaviorclass ProductionAssert { static invariant( condition: boolean, message: string, context?: Record<string, unknown> ): asserts condition { if (!condition) { const error = new InvariantViolationError(message, context); // Always log - this is a bug, we need visibility logger.error('Invariant violation', { message, context, stack: error.stack, }); // Alert monitoring systems metrics.increment('invariant_violations', { message }); // In production, we might choose to: // Option A: Throw (fail-fast) - recommended for critical paths // Option B: Report to error tracking and attempt recovery if (config.isProduction && config.gracefulDegradation) { // Report but don't crash - use for non-critical paths errorTracking.captureException(error); } else { throw error; } } }} // Usagefunction calculateDiscount(order: Order, rules: DiscountRule[]): Money { // This should never happen if our rule engine is correct ProductionAssert.invariant( rules.length > 0, 'Discount calculation invoked with no rules', { orderId: order.id, ruleCount: rules.length } ); const discount = rules.reduce( (sum, rule) => sum.add(rule.apply(order)), Money.zero() ); // Post-condition: discount cannot exceed order total ProductionAssert.invariant( discount.isLessThanOrEqual(order.total), 'Discount exceeds order total', { discount: discount.toString(), total: order.total.toString() } ); return discount;}You now understand the crucial distinction between assertions and validation, and when to apply each. Next, we'll explore the fail-fast principle—a foundational philosophy that guides how systems should respond when things go wrong.