Defensive programming: How to write "robust" rather than "fragile" code?
Defensive programming is a coding philosophy that anticipates and handles potential issues, with its core principle being the assumption that all external inputs may be erroneous and system dependencies may fail at any time. Through boundary checks, exception handling, default value processing, and other means, it ensures that code can remain stable or degrade gracefully under unexpected circumstances.
Core Principles of Defensive Programming
1. Trust No External Input
All data from users, APIs, or local storage should be treated as potential threats. Form inputs, URL parameters, and third-party API responses must undergo strict validation:
// Bad practice: Directly using URL parameters
const productId = window.location.search.split('=')[1]
// Defensive approach
function getSafeProductId() {
const params = new URLSearchParams(window.location.search)
const id = params.get('id')
return /^\d+$/.test(id) ? parseInt(id) : null
}
2. Minimize the Impact of Failures
When a module fails, it should act like a circuit breaker rather than causing a chain reaction:
// Fragile design: A function handling multiple responsibilities
function processUserData(rawData) {
const data = JSON.parse(rawData)
updateDashboard(data.stats)
saveToLocalStorage(data.prefs)
renderUserProfile(data.user)
}
// Defensive improvement: Separation of concerns + error isolation
function safeParse(json) {
try {
return { success: true, data: JSON.parse(json) }
} catch {
return { success: false, error: 'INVALID_JSON' }
}
}
function processUserData(rawData) {
const { success, data, error } = safeParse(rawData)
if (!success) return handleError(error)
// Independent error handling for each module
try { updateDashboard(data?.stats) } catch(e) { console.error(e) }
try { saveToLocalStorage(data?.prefs) } catch(e) { console.error(e) }
try { renderUserProfile(data?.user) } catch(e) { console.error(e) }
}
Frontend-Specific Defensive Strategies
1. DOM Operation Protection
The browser environment is highly unpredictable, and elements may be modified by other scripts:
// Traditional approach
document.getElementById('submit-btn').addEventListener('click', handler)
// Enhanced defensive approach
function safeAddListener(selector, eventType, handler, options) {
const element = document.querySelector(selector)
if (!element || !element.addEventListener) {
return false
}
element.addEventListener(eventType, handler, options)
return true
}
safeAddListener('#submit-btn', 'click', () => {
// Event handling logic
}, { once: true })
2. API Communication Handling
Network requests must account for timeouts, interruptions, and malformed data:
async function fetchWithFallback(url, options = {}) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), options.timeout || 10000)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const contentType = response.headers.get('content-type')
if (!contentType?.includes('application/json')) {
throw new Error('Invalid content type')
}
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
console.warn('Request timed out')
return cachedData || null
}
throw error
} finally {
clearTimeout(timeoutId)
}
}
Type-Safety Defensive Techniques
1. Runtime Type Checking
Even with TypeScript, compile-time type checks may fail at runtime:
// User configuration validation
function validateConfig(config) {
const schema = {
theme: value => ['light', 'dark'].includes(value),
fontSize: value => Number.isInteger(value) && value >= 12 && value <= 24,
notifications: value => typeof value === 'boolean'
}
return Object.entries(schema).every(([key, validator]) => {
return validator(config[key])
})
}
// Usage example
const userConfig = JSON.parse(localStorage.getItem('config') || '{}')
if (!validateConfig(userConfig)) {
resetToDefaultConfig()
}
2. Optional Chaining and Nullish Coalescing
Modern JavaScript syntax provides cleaner defensive patterns:
// Old defensive code
const street = user && user.address && user.address.street
// Modern equivalent
const street = user?.address?.street ?? 'Unknown'
// Function call protection
api.getUserInfo?.().then(...)
Immutable Data Practices
1. Avoid Direct State Mutations
Frontend framework state management requires special attention:
// Vue example
data() {
return {
user: {
name: '',
permissions: []
}
}
},
methods: {
// Unsafe approach
addPermission(perm) {
this.user.permissions.push(perm)
},
// Defensive approach
safeAddPermission(perm) {
this.user = {
...this.user,
permissions: [...this.user.permissions, perm]
}
}
}
2. Deep Freeze Critical Configurations
Prevent accidental modifications to core configuration objects:
function deepFreeze(obj) {
Object.keys(obj).forEach(prop => {
if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) {
deepFreeze(obj[prop])
}
})
return Object.freeze(obj)
}
const config = deepFreeze({
api: {
baseURL: 'https://api.example.com',
timeout: 5000
}
})
// Any subsequent modification attempts will throw errors in strict mode
config.api.timeout = 10000 // TypeError
The Art of Error Handling
1. Categorized Error Handling
Different errors require different handling levels:
class NetworkError extends Error {
constructor(message) {
super(message)
this.name = 'NetworkError'
this.isRecoverable = true
}
}
class AuthError extends Error {
constructor(message) {
super(message)
this.name = 'AuthError'
this.requiresRelogin = true
}
}
async function fetchProtectedData() {
try {
const response = await fetch('/api/protected')
if (response.status === 401) {
throw new AuthError('Session expired')
}
return response.json()
} catch (error) {
if (error.name === 'AuthError') {
showLoginModal()
} else if (error.name === 'NetworkError') {
retryAfterDelay()
} else {
logErrorToService(error)
throw error
}
}
}
2. Error Boundary Design
React error boundary component example:
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error, info) {
logErrorToService(error, info.componentStack)
}
render() {
if (this.state.hasError) {
return (
<div className="fallback-ui">
<h2>Something went wrong</h2>
<button onClick={() => window.location.reload()}>
Refresh Page
</button>
</div>
)
}
return this.props.children
}
}
// Usage
<ErrorBoundary>
<UnstableComponent/>
</ErrorBoundary>
Performance Protection Measures
1. Debouncing and Throttling
Defensive strategies for high-frequency events:
function createDebouncer(delay = 300) {
let timer = null
return function(fn) {
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
try {
fn()
} catch (e) {
console.error('Debounced function error:', e)
}
}, delay)
}
}
// Usage example
const searchDebouncer = createDebouncer(500)
inputElement.addEventListener('input', () => {
searchDebouncer(() => searchAPI(inputElement.value))
})
2. Memory Leak Prevention
Common SPA issue defenses:
// Cleanup on component unmount
useEffect(() => {
const controller = new AbortController()
fetchData({ signal: controller.signal })
return () => {
controller.abort()
clearAllTimeouts() // Custom cleanup function
window.removeEventListener('resize', handleResize)
}
}, [])
// HOC for memory leak detection
function withMemoryLeakDetection(WrappedComponent) {
return function(props) {
const [leakReport, setLeakReport] = useState(null)
useEffect(() => {
const initialMemory = performance.memory.usedJSHeapSize
return () => {
const delta = performance.memory.usedJSHeapSize - initialMemory
if (delta > 1024 * 1024) { // Potential leak if over 1MB
setLeakReport(`Possible leak: ${Math.round(delta/1024)}KB`)
}
}
}, [])
return (
<>
<WrappedComponent {...props} />
{leakReport && <div className="leak-warning">{leakReport}</div>}
</>
)
}
}
Environment Difference Handling
1. Browser Feature Detection
Avoid direct browser type checking:
// Not recommended
if (navigator.userAgent.includes('Chrome')) {
useChromeSpecificAPI()
}
// Defensive approach
if ('IntersectionObserver' in window) {
// Use modern API
} else {
// Fallback solution
}
// Progressive enhancement loading strategy
function loadPolyfillIfNeeded(feature, polyfillUrl) {
if (!feature in window) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = polyfillUrl
script.onload = resolve
script.onerror = reject
document.body.appendChild(script)
})
}
return Promise.resolve()
}
await loadPolyfillIfNeeded('fetch', '/polyfills/fetch.js')
2. Environment Variable Protection
Handling build-time injected variables:
// Unsafe approach
const apiUrl = process.env.API_URL
// Defensive handling
function getEnvVar(key) {
const value = process.env[key]
if (value === undefined) {
if (import.meta.env.PROD) {
throw new Error(`Missing required env var: ${key}`)
}
return getDefaultValue(key)
}
return value
}
const apiUrl = getEnvVar('API_URL') || 'https://api.fallback.com'
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn