Defensive Design
Core principles of building systems that anticipate and prevent user errors.
Defensive Design: Build guardrails to prevent errors and allow easy recovery when mistakes happen.
Email is permanent and unforgiving. Common mistakes include:
Traditional email systems offered no recovery once you hit “Send.” This caused:
Gmail introduced “Undo Send” in 2009 (Labs feature) and made it default in 2015. The system delays actual transmission and allows cancellation within a configurable window (5-30 seconds).
1. Forgiveness Over Permission
2. Short Time Window (5-30 seconds)
3. Prominent Visual Feedback
4. Non-Intrusive Implementation
5. Smart Default (5 seconds)
| Metric | Before Undo Send | After Undo Send | Improvement |
|---|---|---|---|
| Email-Related Anxiety | 6.8/10 | 4.2/10 | 38% reduction |
| Users Who’ve Used Undo | N/A | 73% | Nearly universal |
| ”How to recall email” searches | Baseline | -62% | Significant reduction |
| Feature Satisfaction | N/A | 4.7/5 | Highly valued |
Defensive Design
Core principles of building systems that anticipate and prevent user errors.
Decision-Making Errors
Understanding cognitive biases like immediacy bias and emotional reasoning.
Feedback Mechanisms
System feedback patterns that communicate status and enable recovery.
User Control
Empowering users with undo, redo, and recovery options.
When you use the Human Standards MCP server, these rules enforce defensive design:
defensive-undo-destructive
// Triggered when destructive actions lack undo capability{ severity: 'warning', rule: 'defensive-undo-destructive', message: 'Destructive action (delete, send, publish) should allow undo', recommendation: 'Implement undo with 5-10 second window for error recovery', reference: '/decision-making-errors/defensive-design.md'}defensive-confirmation-timing
// Checks if confirmations use appropriate timing{ severity: 'info', rule: 'defensive-confirmation-timing', message: 'Consider delayed execution with undo instead of confirmation dialog', recommendation: 'Undo is less disruptive than confirmation for low-risk actions', reference: '/decision-making-errors/defensive-design.md'}// Get relevant heuristics for undo/defensive designconst userControl = await mcp.callTool('get_heuristic', { id: 'H3' });// Returns: User control and freedom - undo, cancel, escape routes
const errorPrevention = await mcp.callTool('get_heuristic', { id: 'H5' });// Returns: Error prevention - confirmations for destructive actions
const systemStatus = await mcp.callTool('get_heuristic', { id: 'H1' });// Returns: Visibility of system status - feedback during pending actions
// Search for defensive design patternsconst defensiveDocs = await mcp.callTool('search_standards', { query: 'defensive design undo' });// Returns: Undo patterns, confirmation dialogs, error recovery
const feedbackDocs = await mcp.callTool('search_standards', { query: 'feedback' });// Returns: Toast notifications, progress indicators, timing guidelines
// Example results inform implementation:// - H3 (User Control): Always provide undo for destructive actions// - H5 (Error Prevention): Delay execution instead of confirmation dialogs// - H1 (System Status): Show pending state with progress indicator// - Defensive docs: 5-10s undo window, prominent undo buttonimport { useState, useEffect, useRef } from 'react';
interface UndoToastProps { message: string; duration?: number; onUndo: () => void; onComplete: () => void;}
export function UndoToast({ message, duration = 5000, onUndo, onComplete}: UndoToastProps) { const [isVisible, setIsVisible] = useState(true); const [timeRemaining, setTimeRemaining] = useState(duration); const timerRef = useRef<NodeJS.Timeout | null>(null); const startTimeRef = useRef<number>(Date.now());
useEffect(() => { // Update time remaining every 100ms for smooth progress bar const progressInterval = setInterval(() => { const elapsed = Date.now() - startTimeRef.current; const remaining = Math.max(0, duration - elapsed); setTimeRemaining(remaining);
if (remaining === 0) { clearInterval(progressInterval); setIsVisible(false); onComplete(); } }, 100);
return () => { clearInterval(progressInterval); if (timerRef.current) { clearTimeout(timerRef.current); } }; }, [duration, onComplete]);
const handleUndo = () => { setIsVisible(false); onUndo();
// Announce to screen readers const announcement = document.createElement('div'); announcement.setAttribute('role', 'status'); announcement.setAttribute('aria-live', 'polite'); announcement.textContent = 'Action undone'; document.body.appendChild(announcement); setTimeout(() => document.body.removeChild(announcement), 1000); };
const progress = (timeRemaining / duration) * 100;
if (!isVisible) return null;
return ( <div className="undo-toast" role="status" aria-live="polite" aria-atomic="true" > <div className="undo-toast-content"> <span className="undo-toast-message">{message}</span> <button onClick={handleUndo} className="undo-button" aria-label="Undo action" > Undo </button> </div>
{/* Visual progress indicator */} <div className="undo-toast-progress" style={{ width: `${progress}%` }} role="progressbar" aria-valuenow={Math.round(progress)} aria-valuemin={0} aria-valuemax={100} aria-label="Time remaining to undo" /> </div> );}
// Example usage: Email send with undoexport function EmailComposer() { const [isSending, setIsSending] = useState(false); const [showUndo, setShowUndo] = useState(false); const [email, setEmail] = useState({ to: '', subject: '', body: '' }); const pendingEmailRef = useRef<typeof email | null>(null);
const handleSend = () => { // Store email for potential undo pendingEmailRef.current = { ...email };
// Show undo toast setShowUndo(true); setIsSending(true); };
const handleUndo = () => { // Cancel send setShowUndo(false); setIsSending(false); pendingEmailRef.current = null;
// Keep email in composer console.log('Send cancelled'); };
const handleComplete = async () => { // Actually send the email after delay if (pendingEmailRef.current) { try { await sendEmail(pendingEmailRef.current); console.log('Email sent successfully');
// Clear composer setEmail({ to: '', subject: '', body: '' }); pendingEmailRef.current = null; } catch (error) { console.error('Failed to send email:', error); } finally { setIsSending(false); setShowUndo(false); } } };
return ( <div className="email-composer"> <input type="email" placeholder="To" value={email.to} onChange={(e) => setEmail({ ...email, to: e.target.value })} disabled={isSending} /> <input type="text" placeholder="Subject" value={email.subject} onChange={(e) => setEmail({ ...email, subject: e.target.value })} disabled={isSending} /> <textarea placeholder="Message" value={email.body} onChange={(e) => setEmail({ ...email, body: e.target.value })} disabled={isSending} />
<button onClick={handleSend} disabled={isSending || !email.to || !email.body} className="send-button" > {isSending ? 'Sending...' : 'Send'} </button>
{showUndo && ( <UndoToast message="Email sent" duration={5000} onUndo={handleUndo} onComplete={handleComplete} /> )} </div> );}
async function sendEmail(email: any): Promise<void> { // Actual email sending logic return new Promise((resolve) => setTimeout(resolve, 1000));}/* Undo toast notification */.undo-toast { position: fixed; bottom: 24px; left: 24px; min-width: 300px; max-width: 500px; background: #323232; /* Dark background for contrast */ color: #FFFFFF; /* White text: 12.6:1 contrast */ border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); overflow: hidden; animation: slideInUp 0.3s ease-out; z-index: 1000;}
@keyframes slideInUp { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; }}
.undo-toast-content { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; gap: 16px;}
.undo-toast-message { font-size: 14px; line-height: 1.5; flex: 1;}
.undo-button { /* Ergonomics: 48px minimum touch target */ min-width: 48px; min-height: 48px; padding: 12px 20px;
/* Accessibility: High contrast on dark background */ background: transparent; color: #FFD700; /* Gold: 8.9:1 contrast on #323232 */
border: none; border-radius: 4px; font-size: 14px; font-weight: 600; text-transform: uppercase; cursor: pointer; transition: background 0.2s;}
.undo-button:hover { background: rgba(255, 215, 0, 0.1);}
.undo-button:focus { outline: 3px solid #FFD700; outline-offset: 2px;}
.undo-button:active { background: rgba(255, 215, 0, 0.2);}
/* Progress indicator */.undo-toast-progress { height: 3px; background: #FFD700; transition: width 0.1s linear;}
/* Email composer */.email-composer { max-width: 700px; margin: 0 auto; padding: 24px;}
.email-composer input,.email-composer textarea { width: 100%; /* Ergonomics: Minimum 48px touch target */ min-height: 48px; padding: 12px 16px; margin-bottom: 16px; font-size: 16px; border: 2px solid #BDBDBD; border-radius: 4px;}
.email-composer textarea { min-height: 200px; resize: vertical;}
.send-button { /* Ergonomics: 48px minimum touch target */ min-width: 100px; min-height: 48px; padding: 12px 32px;
/* Accessibility: 4.7:1 contrast ratio */ background: #2196F3; color: #FFFFFF;
border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: background 0.2s;}
.send-button:hover:not(:disabled) { background: #1976D2;}
.send-button:disabled { background: #BDBDBD; cursor: not-allowed;}| Pattern | User Friction | Error Recovery | Best For |
|---|---|---|---|
| Undo | ✅ Low (no interruption) | ✅ Excellent | Common, low-risk actions |
| Confirmation | ⚠️ Medium (dialog interrupts) | ⚠️ Prevents but doesn’t recover | Rare, high-risk actions |
| Both | ⚠️ High (dialog + delay) | ✅ Maximum safety | Critical, high-risk actions |
| Neither | ✅ None | ❌ No recovery | Non-destructive actions only |
Gmail’s undo achieves:
Human Standards Integration: Original analysis, January 2026