Category: logicDifficulty: MediumPublished: 2024-12-18
Understanding and Fixing Boundary Condition Errors
Boundary condition errors occur when code fails to handle edge cases or extreme values correctly. These bugs can be particularly tricky because they often only manifest under specific conditions that might not be immediately obvious during testing.
Common Boundary Condition Scenarios
1. Array Bounds
// Problem: Not checking array bounds function getLastElement(array) { return array[array.length]; // Returns undefined, array[length-1] is last element } // Solution: Proper bounds checking function getLastElementFixed(array) { if (!Array.isArray(array) || array.length === 0) { throw new Error('Invalid array input'); } return array[array.length - 1]; }
2. Numeric Ranges
// Problem: Not handling numeric limits function calculatePercentage(value, total) { return (value / total) * 100; // Fails for total = 0 } // Solution: Range validation function calculatePercentageSafe(value, total) { if (typeof value !== 'number' || typeof total !== 'number') { throw new TypeError('Inputs must be numbers'); } if (total === 0) { throw new Error('Cannot calculate percentage with zero total'); } if (value < 0 || total < 0) { throw new Error('Negative values not allowed'); } return (value / total) * 100; }
3. String Operations
// Problem: Not handling empty or long strings function truncateString(str, maxLength) { return str.substring(0, maxLength) + '...'; // Adds ... even if no truncation needed } // Solution: Proper string length handling function truncateStringFixed(str, maxLength) { if (typeof str !== 'string') { throw new TypeError('Input must be a string'); } if (maxLength < 0) { throw new Error('Max length must be non-negative'); } if (str.length <= maxLength) { return str; } return str.substring(0, maxLength) + '...'; }
Prevention Strategies
1. Input Validation
class InputValidator { static validateRange(value, min, max, name = 'Value') { if (typeof value !== 'number' || isNaN(value)) { throw new TypeError(`${name} must be a valid number`); } if (value < min || value > max) { throw new RangeError( `${name} must be between ${min} and ${max}, got ${value}` ); } return value; } static validateArrayIndex(array, index, name = 'Index') { if (!Array.isArray(array)) { throw new TypeError('Expected an array'); } if (!Number.isInteger(index)) { throw new TypeError(`${name} must be an integer`); } if (index < 0 || index >= array.length) { throw new RangeError( `${name} out of bounds: ${index} (array length: ${array.length})` ); } return index; } static validateString(str, options = {}) { const { minLength = 0, maxLength = Infinity, name = 'String' } = options; if (typeof str !== 'string') { throw new TypeError(`${name} must be a string`); } if (str.length < minLength) { throw new RangeError( `${name} must be at least ${minLength} characters long` ); } if (str.length > maxLength) { throw new RangeError( `${name} must not exceed ${maxLength} characters` ); } return str; } } // Usage function processData(array, index) { InputValidator.validateArrayIndex(array, index); return array[index]; }
2. Range Checker
class RangeChecker { static isInRange(value, min, max) { return value >= min && value <= max; } static isInRangeExclusive(value, min, max) { return value > min && value < max; } static clamp(value, min, max) { return Math.min(Math.max(value, min), max); } static normalizeIndex(index, length) { if (index < 0) { return Math.max(0, length + index); } return Math.min(index, length - 1); } } // Usage function getArrayElement(array, index) { const normalizedIndex = RangeChecker.normalizeIndex(index, array.length); return array[normalizedIndex]; }
3. Boundary Guards
class BoundaryGuard { static guardArray(array, operation) { if (!Array.isArray(array) || array.length === 0) { return null; } return operation(array); } static guardDivision(numerator, denominator, fallback = 0) { if (denominator === 0) { return fallback; } return numerator / denominator; } static guardString(str, operation, fallback = '') { if (typeof str !== 'string' || str.length === 0) { return fallback; } return operation(str); } } // Usage const result = BoundaryGuard.guardDivision(10, 0, Infinity); console.log(result); // Infinity instead of NaN
Testing Strategies
1. Boundary Tests
describe('Boundary Tests', () => { it('should handle array boundaries correctly', () => { const array = [1, 2, 3]; // Test empty array expect(() => getLastElementFixed([])).toThrow(); // Test valid indices expect(getLastElementFixed(array)).toBe(3); // Test invalid input expect(() => getLastElementFixed(null)).toThrow(); }); it('should handle numeric boundaries', () => { // Test zero division expect(() => calculatePercentageSafe(5, 0)).toThrow(); // Test negative values expect(() => calculatePercentageSafe(-1, 100)).toThrow(); // Test valid values expect(calculatePercentageSafe(50, 100)).toBe(50); }); });
2. Range Tests
describe('Range Tests', () => { it('should validate numeric ranges', () => { // Test in-range values expect(RangeChecker.isInRange(5, 0, 10)).toBe(true); // Test boundary values expect(RangeChecker.isInRange(0, 0, 10)).toBe(true); expect(RangeChecker.isInRange(10, 0, 10)).toBe(true); // Test out-of-range values expect(RangeChecker.isInRange(-1, 0, 10)).toBe(false); expect(RangeChecker.isInRange(11, 0, 10)).toBe(false); }); it('should clamp values correctly', () => { expect(RangeChecker.clamp(-1, 0, 10)).toBe(0); expect(RangeChecker.clamp(5, 0, 10)).toBe(5); expect(RangeChecker.clamp(11, 0, 10)).toBe(10); }); });
3. Edge Case Tests
describe('Edge Case Tests', () => { it('should handle string edge cases', () => { const options = { minLength: 2, maxLength: 10 }; // Test empty string expect(() => InputValidator.validateString('', options) ).toThrow(); // Test minimum length expect(() => InputValidator.validateString('a', options) ).toThrow(); // Test maximum length expect(() => InputValidator.validateString('a'.repeat(11), options) ).toThrow(); // Test valid strings expect( InputValidator.validateString('hello', options) ).toBe('hello'); }); });
Best Practices
-
Always Validate Input
// Bad function process(value) { return value * 2; } // Good function process(value) { if (typeof value !== 'number' || isNaN(value)) { throw new TypeError('Expected a number'); } return value * 2; }
-
Use Guard Clauses
// Bad function divide(a, b) { return a / b; } // Good function divide(a, b) { if (b === 0) { throw new Error('Division by zero'); } return a / b; }
-
Handle Special Cases
// Bad function getFirstChar(str) { return str[0]; } // Good function getFirstChar(str) { if (!str || typeof str !== 'string') { return ''; } return str[0] || ''; }
Common Gotchas
-
Array Length vs Index
// Common mistake const arr = [1, 2, 3]; for (let i = 0; i <= arr.length; i++) { // <= is wrong console.log(arr[i]); // Undefined on last iteration } // Correct for (let i = 0; i < arr.length; i++) { // < is correct console.log(arr[i]); }
-
Floating Point Precision
// Unexpected behavior console.log(0.1 + 0.2 === 0.3); // false // Solution: Use epsilon comparison function nearlyEqual(a, b, epsilon = Number.EPSILON) { return Math.abs(a - b) < epsilon; } console.log(nearlyEqual(0.1 + 0.2, 0.3)); // true
-
String Length vs Index
// Unicode issues const str = '👋'; console.log(str.length); // 2 (surrogate pair) // Solution: Use Array.from or spread console.log([...str].length); // 1 (correct length)