COMP 305: Object-Oriented Software Design

University of San Diego, Fall 2025

Graceful Degradation

Graceful Degradation is a design approach that starts with a full-featured experience for modern browsers, then provides fallback experiences for older or less capable browsers. The goal is to maintain core functionality even when advanced features aren't available.

The Core Philosophy

"Build for the best experience first, then ensure it still works when features are missing."

Graceful Degradation ensures:

Modern Feature with Multiple Fallbacks

Start with the best experience, then provide progressively simpler fallbacks.

// Modern feature with graceful degradation

class ModernVideoPlayer {
    constructor(videoElement) {
        this.video = videoElement;
        this.setupPlayer();
    }

    setupPlayer() {
        // Check for modern features and degrade gracefully
        if (this.supportsHLS()) {
            this.enableAdaptiveStreaming();
        } else if (this.supportsMP4()) {
            this.enableBasicPlayback();
        } else {
            this.showFallbackMessage();
        }

        // Optional enhancements
        if (this.supportsPictureInPicture()) {
            this.enablePiP();
        }

        if (this.supportsFullscreen()) {
            this.enableFullscreen();
        }
    }

    supportsHLS() {
        return this.video.canPlayType('application/vnd.apple.mpegurl') !== '';
    }

    supportsMP4() {
        return this.video.canPlayType('video/mp4') !== '';
    }

    supportsPictureInPicture() {
        return 'pictureInPictureEnabled' in document;
    }

    supportsFullscreen() {
        return 'requestFullscreen' in this.video;
    }

    enableAdaptiveStreaming() {
        // Best experience: HLS adaptive streaming
        this.video.src = '/video/stream.m3u8';
        console.log('Adaptive streaming enabled');
    }

    enableBasicPlayback() {
        // Fallback: Basic MP4 playback
        this.video.src = '/video/video.mp4';
        console.log('Basic playback enabled');
    }

    showFallbackMessage() {
        // Last resort: Show download link
        this.video.parentElement.innerHTML = `
            <p>Your browser doesn't support video playback.</p>
            <a href="/video/video.mp4" download>Download Video</a>
        `;
    }

    enablePiP() {
        const pipButton = document.createElement('button');
        pipButton.textContent = 'Picture-in-Picture';
        pipButton.onclick = () => this.video.requestPictureInPicture();
        this.video.parentElement.appendChild(pipButton);
    }

    enableFullscreen() {
        const fsButton = document.createElement('button');
        fsButton.textContent = 'Fullscreen';
        fsButton.onclick = () => this.video.requestFullscreen();
        this.video.parentElement.appendChild(fsButton);
    }
}

Fallback Hierarchy

  1. Best - Full featured experience (modern browsers)
  2. Good - Core functionality with reduced features (older browsers)
  3. Basic - Minimal but functional experience (very old browsers)
  4. Message - Clear explanation when feature is unavailable

API and Storage Degradation

Modern APIs often have older equivalents. Detect and fall back gracefully.

// Graceful degradation for API features

class DataFetcher {
    async getData(url) {
        // Try modern fetch API
        if ('fetch' in window) {
            try {
                const response = await fetch(url);
                return await response.json();
            } catch (error) {
                console.warn('Fetch failed, trying XHR');
                return this.getDataXHR(url);
            }
        }

        // Fallback to XMLHttpRequest
        return this.getDataXHR(url);
    }

    getDataXHR(url) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', url);

            xhr.onload = () => {
                if (xhr.status === 200) {
                    resolve(JSON.parse(xhr.responseText));
                } else {
                    reject(new Error(`HTTP ${xhr.status}`));
                }
            };

            xhr.onerror = () => reject(new Error('Network error'));
            xhr.send();
        });
    }
}

// Storage with graceful degradation
class Storage {
    set(key, value) {
        const data = JSON.stringify(value);

        // Try localStorage (modern, persistent)
        try {
            localStorage.setItem(key, data);
            return true;
        } catch (e) {
            console.warn('localStorage failed, trying sessionStorage');
        }

        // Try sessionStorage (fallback, session only)
        try {
            sessionStorage.setItem(key, data);
            return true;
        } catch (e) {
            console.warn('sessionStorage failed, using memory');
        }

        // Last resort: in-memory storage (volatile)
        this.memoryStore = this.memoryStore || {};
        this.memoryStore[key] = data;
        return true;
    }

    get(key) {
        // Try localStorage first
        try {
            const data = localStorage.getItem(key);
            return data ? JSON.parse(data) : null;
        } catch (e) {}

        // Try sessionStorage
        try {
            const data = sessionStorage.getItem(key);
            return data ? JSON.parse(data) : null;
        } catch (e) {}

        // Fallback to memory
        const data = this.memoryStore?.[key];
        return data ? JSON.parse(data) : null;
    }
}

Common API Fallback Chains

CSS Graceful Degradation

Use modern CSS features with fallbacks for older browsers.

/* CSS Graceful Degradation */

/* Modern Grid layout with Flexbox fallback */
.container {
    /* Flexbox fallback (older browsers) */
    display: flex;
    flex-wrap: wrap;
    gap: 1rem; /* May not work in older browsers */
    margin: -0.5rem; /* Emulate gap for old browsers */
}

.container > * {
    flex: 1 1 300px;
    margin: 0.5rem; /* Fallback spacing */
}

/* Modern browsers override with Grid */
@supports (display: grid) {
    .container {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
        margin: 0; /* Reset fallback margin */
    }

    .container > * {
        margin: 0; /* Reset fallback margin */
    }
}

/* Custom Properties with fallbacks */
.button {
    /* Fallback color for browsers without custom properties */
    background: #0066cc;
    color: white;

    /* Modern: use custom properties */
    background: var(--color-primary, #0066cc);
    color: var(--color-text-on-primary, white);
}

/* Modern features with fallbacks */
.card {
    /* Basic shadow for old browsers */
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);

    /* Better shadow for newer browsers */
    box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1),
                0 2px 4px -1px rgba(0,0,0,0.06);
}

@supports (backdrop-filter: blur(10px)) {
    .glass {
        background: rgba(255,255,255,0.8);
        backdrop-filter: blur(10px);
    }
}

@supports not (backdrop-filter: blur(10px)) {
    .glass {
        background: rgba(255,255,255,0.95);
    }
}

CSS Fallback Strategies

Error Handling and Network Resilience

Handle failures gracefully with retries, timeouts, and fallback data.

// Error Handling and Fallbacks

class RobustImageLoader {
    loadImage(src, fallbackSrc, errorCallback) {
        const img = new Image();

        img.onerror = () => {
            console.warn(`Failed to load ${src}, trying fallback`);

            // Try fallback image
            img.onerror = () => {
                console.error('Fallback image also failed');

                // Show placeholder
                this.showPlaceholder(img);

                if (errorCallback) {
                    errorCallback(new Error('All image sources failed'));
                }
            };

            img.src = fallbackSrc;
        };

        img.src = src;
        return img;
    }

    showPlaceholder(img) {
        // Replace with base64 placeholder
        img.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjMwMCI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iI2VlZSIvPjwvc3ZnPg==';
        img.alt = 'Image failed to load';
    }
}

// Network resilience
class ResilientFetch {
    async fetch(url, options = {}) {
        const {
            retries = 3,
            timeout = 5000,
            fallbackData = null
        } = options;

        for (let attempt = 1; attempt <= retries; attempt++) {
            try {
                const controller = new AbortController();
                const timeoutId = setTimeout(() => controller.abort(), timeout);

                const response = await fetch(url, {
                    ...options,
                    signal: controller.signal
                });

                clearTimeout(timeoutId);

                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}`);
                }

                return await response.json();

            } catch (error) {
                console.warn(`Attempt ${attempt} failed:`, error.message);

                if (attempt === retries) {
                    // All retries exhausted
                    if (fallbackData !== null) {
                        console.log('Using fallback data');
                        return fallbackData;
                    }
                    throw error;
                }

                // Wait before retry (exponential backoff)
                await this.sleep(Math.pow(2, attempt) * 1000);
            }
        }
    }

    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

Offline Support

Degrade gracefully when network connection is lost. Queue actions and sync when reconnected.

// Offline Support with Graceful Degradation

class OfflineCapableApp {
    constructor() {
        this.checkOnlineStatus();
        this.setupEventListeners();
        this.registerServiceWorker();
    }

    checkOnlineStatus() {
        if (!navigator.onLine) {
            this.enableOfflineMode();
        }
    }

    setupEventListeners() {
        window.addEventListener('online', () => {
            this.enableOnlineMode();
            this.syncPendingData();
        });

        window.addEventListener('offline', () => {
            this.enableOfflineMode();
        });
    }

    enableOfflineMode() {
        document.body.classList.add('offline');
        this.showOfflineBanner();

        // Disable features that require connection
        this.disableRealTimeFeatures();

        // Enable offline features
        this.enableLocalCaching();
        this.queuePendingActions();
    }

    enableOnlineMode() {
        document.body.classList.remove('offline');
        this.hideOfflineBanner();

        // Re-enable online features
        this.enableRealTimeFeatures();
        this.clearLocalCache();
    }

    showOfflineBanner() {
        const banner = document.createElement('div');
        banner.className = 'offline-banner';
        banner.textContent = 'You are offline. Some features are unavailable.';
        document.body.prepend(banner);
    }

    hideOfflineBanner() {
        document.querySelector('.offline-banner')?.remove();
    }

    async registerServiceWorker() {
        if ('serviceWorker' in navigator) {
            try {
                await navigator.serviceWorker.register('/sw.js');
                console.log('Service Worker registered for offline support');
            } catch (error) {
                console.warn('Service Worker registration failed:', error);
                // App still works, just without offline caching
            }
        } else {
            console.log('Service Workers not supported - offline features disabled');
        }
    }

    disableRealTimeFeatures() {
        // Close WebSocket connections
        // Disable live updates
        // Show cached data
    }

    enableRealTimeFeatures() {
        // Reconnect WebSockets
        // Enable live updates
        // Fetch fresh data
    }

    enableLocalCaching() {
        // Cache API responses locally
        // Store user actions for later
    }

    queuePendingActions() {
        // Queue writes for when online
    }

    async syncPendingData() {
        // Upload queued actions
        // Sync with server
        console.log('Syncing data after reconnection...');
    }

    clearLocalCache() {
        // Clean up temporary offline data
    }
}

Offline Strategies

Comprehensive Feature Detection

Detect browser capabilities and adapt functionality accordingly.

// Comprehensive Feature Detection and Fallbacks

class FeatureManager {
    constructor() {
        this.features = {
            // Storage
            localStorage: this.testLocalStorage(),
            indexedDB: 'indexedDB' in window,

            // Network
            fetch: 'fetch' in window,
            serviceWorker: 'serviceWorker' in navigator,

            // Media
            webRTC: 'RTCPeerConnection' in window,
            mediaRecorder: 'MediaRecorder' in window,

            // Graphics
            webGL: this.testWebGL(),
            canvas: this.testCanvas(),

            // Sensors
            geolocation: 'geolocation' in navigator,
            deviceOrientation: 'DeviceOrientationEvent' in window,

            // Performance
            intersectionObserver: 'IntersectionObserver' in window,
            performanceObserver: 'PerformanceObserver' in window,

            // Misc
            notifications: 'Notification' in window,
            vibration: 'vibrate' in navigator
        };

        this.logCapabilities();
    }

    testLocalStorage() {
        try {
            localStorage.setItem('test', 'test');
            localStorage.removeItem('test');
            return true;
        } catch (e) {
            return false;
        }
    }

    testWebGL() {
        try {
            const canvas = document.createElement('canvas');
            return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
        } catch (e) {
            return false;
        }
    }

    testCanvas() {
        const canvas = document.createElement('canvas');
        return !!(canvas.getContext && canvas.getContext('2d'));
    }

    logCapabilities() {
        console.group('Browser Capabilities');
        Object.entries(this.features).forEach(([feature, supported]) => {
            console.log(`${feature}: ${supported ? '✓' : '✗'}`);
        });
        console.groupEnd();
    }

    has(feature) {
        return this.features[feature] || false;
    }

    requires(features) {
        return features.every(feature => this.has(feature));
    }
}

// Usage
const features = new FeatureManager();

if (features.has('fetch')) {
    // Use modern fetch
} else {
    // Fall back to XHR
}

if (features.requires(['geolocation', 'notifications'])) {
    // Enable location-based notifications
}

Benefits

Best Practices

When to Use Graceful Degradation

Graceful Degradation vs Progressive Enhancement

Graceful Degradation Progressive Enhancement
Start with full experience Start with baseline
Degrade when features missing Enhance when features available
Modern browser focus Universal compatibility focus
Add fallbacks after Add enhancements on top

Best Approach: Use both! Progressive Enhancement for core functionality, Graceful Degradation for advanced features.

Testing Degradation

Verify fallbacks work correctly: