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:
Google promoted Undo Send from Gmail Labs to a formal Gmail web setting in June 2015 (Google Workspace update). Current Gmail documentation describes a short cancellation window: immediately after sending, the interface shows “Message sent” with Undo and View message actions, and desktop users can choose 5, 10, 20, or 30 seconds (Gmail Help).
This is immediate cancellation, not a promise that Gmail can retrieve a message after the window has expired.
1. Forgiveness Over Permission
Google’s current product guide shows the message returning so the sender can edit it after selecting Undo (Google: How to undo send).
2. Short Time Window (5-30 seconds)
3. Prominent Visual Feedback
4. Non-Intrusive Implementation
5. Configurable default
The following are design inferences from the documented cancellation flow, not measured Gmail outcomes.
Google’s documentation establishes the feature history, notification actions, default, and available cancellation periods. It does not publish the usage, anxiety, search-volume, or satisfaction figures previously shown here. Those figures have been removed.
To evaluate this pattern in another product, measure:
| Measure | Method |
|---|---|
| Undo use | Eligible actions followed by Undo, segmented by window length |
| Recovery success | Undo attempts completed before irreversible processing |
| Accidental action rate | Confirmed mistakes per eligible action before and after launch |
| Added latency | Time until downstream processing is finalized |
| Comprehension | Whether users understand the action is cancellable only during the window |
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/'}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/'}// 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 |
For Gmail specifically, the primary evidence supports a non-modal Undo action and a configurable 5–30 second window. Outcome claims still require product analytics or a published study; the interaction design alone does not establish usage, satisfaction, or error reduction.
Human Standards Integration: Analysis updated against cited primary documentation, August 2026