Failing with Dignity: A Deep Dive into Graceful Degradation
Everything fails, all the time - Werner Vogels, CTO at Amazon.com.
In one of the previous posts we introduced Eight Pillars of Fault-tolerant Systems and today we will discuss "Graceful Degradation". In this article, we're going to dive into the depths of this concept and discuss its importance in software engineering.
What is Graceful Degradation?
Graceful degradation refers to a design philosophy that prioritizes the maintenance of functionality, albeit at a reduced level, when parts of a system fail or are unavailable. The goal is to prevent the entire system from failing when a specific feature encounters an issue.
In a software application, for instance, this could mean ensuring that the core features of the application remain functional even if secondary features become unavailable or perform poorly.
Why is Graceful Degradation Important?
The user experience should always be front-and-center when building software. When an application suffers from feature failure and does not implement graceful degradation, the user experience takes a significant hit. This could lead to the loss of users, loss of revenue, and damage to your brand.
Implementing graceful degradation not only minimizes the negative impact on users when inevitable failures happen but also creates an impression of reliability and robustness about your software, even in adverse situations.
Implementing Graceful Degradation
Let's explore some strategies to implement graceful degradation:
1. Exception Handling
Robust exception handling is the first step towards graceful degradation. Whenever an error occurs, your application should be capable of catching it and responding appropriately. This might mean displaying a user-friendly error message, attempting an alternative operation, or falling back to a safe state. Here is a quick example in javascript:
try {
// code that might throw an exception
movieRecommendations(user);
} catch (error) {
// handle the error
console.error('An error occurred: ', error);
// A fallback operation
top10Movies();
}
2. Redundancy
Having redundant components in your system can allow you to maintain functionality even when one component fails. For example, in a distributed system, you could have multiple instances of a service running. If one instance fails, the load balancer can simply redirect requests to the other instances. Find out more about this concept in our previous blog post.
3. Circuit Breaker Pattern
In a microservices architecture, the circuit breaker pattern can be crucial in preventing a single service failure from cascading and bringing down the entire system. When a network call to a service fails a certain number of times, the circuit breaker "trips" and future calls are automatically redirected to a fallback operation until the failing service recovers.
4. Feature Flags
Feature flags allow you to turn features of your application on or off at runtime. If a new feature is causing issues, you can use a feature flag to quickly turn it off without needing to redeploy your application.
import FeatureFlags from './FeatureFlags';
function performHeavyOperation() {
// Use the feature flag to determine if the operation should be performed.
if (FeatureFlags.isEnabled('enableHeavyOperation')) {
console.log('Performing heavy operation...');
// Code for the operation goes here.
} else {
console.log('Heavy operation is currently disabled.');
// Graceful degradation: perform a lighter operation, show an error message, etc.
}
}
performHeavyOperation();
The isEnabled
function checks if a given feature flag is enabled. In this case, if the enableHeavyOperation
flag is not set, the application will avoid performing the "heavy operation" and will instead degrade gracefully.
5. Load Shedding
In situations where your application is under extremely high load, you might decide to temporarily disable non-critical features or drop a certain amount of requests in order to preserve the functionality of critical ones. This is known as load shedding. In the example below we will reject non-critical requests if the load is too high:
// Value of HIGH_LOAD variable would typically be determined dynamically
function handleRequest(request) {
if (HIGH_LOAD) {
if (isNonCriticalRequest(request)) {
// reject non-critical requests
return rejectRequest(request);
}
}
// handle the request
processRequest(request);
}
function isNonCriticalRequest(request) {
// logic to determine if a request is non-critical
}
function rejectRequest(request) {
// logic to reject a request
}
function processRequest(request) {
// logic to process a request
}
Let's build! Graceful Degradation Example
We will walk you through a step-by-step guide on how to implement a simple graceful degradation strategy in JavaScript using the Fetch API.
Let's say we want to build a page that returns a user a random joke. We'll fetch a joke from an API, and if the API is unavailable, we'll display a predefined joke instead.
First, let's set up a simple Express server. Create a new directory for your project and initialize it with npm and install some dependencies:
$ mkdir graceful-degradation-example
$ cd graceful-degradation-example
$ npm init -y
$ npm install express node-fetch
Create a new file named server.js
and add the following code:
const express = require('express');
const app = express();
const port = 3000;
// Serve static files from the 'public' directory
app.use(express.static('public'));
// API endpoint
app.get('/api/data', async (req, res) => {
try {
// Dynamic import of node-fetch
const fetch = (await import('node-fetch')).default;
// Try to fetch a random joke
const response = await fetch('https://official-joke-api.appspot.com/random_joke');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
res.json(data);
} catch (error) {
console.error('Error fetching data: ', error);
// Fallback data
const fallbackData = {
setup: 'Did you hear the story about the cheese that saved the world?',
punchline: 'It was legend dairy.'
};
res.json(fallbackData);
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
This code sets up an Express server that serves static files from a public
directory. It also defines an API endpoint that fetches a random joke from https://official-joke-api.appspot.com/random_joke
. If the fetch operation fails, it sends a predefined joke as a fallback.
Next, create a public
directory in your project root. Inside the public
directory, create two files: index.html
and client.js
.
Add the following code to index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Graceful Degradation Example</title>
<script src="client.js"></script>
</head>
<body>
<h1>Graceful Degradation Example</h1>
<button onclick="fetchJoke()">Fetch Joke</button>
<p id="joke"></p>
</body>
</html>
And add the following code to client.js
:
async function fetchJoke() {
const jokeElement = document.getElementById('joke');
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const joke = await response.json();
jokeElement.textContent = joke.setup + ' ' + joke.punchline;
} catch (error) {
console.error('Error fetching joke: ', error);
jokeElement.textContent = 'An error occurred while fetching the joke.';
}
}
This code defines a function fetchJoke
that fetches a joke from our API endpoint and displays it on the webpage. If the fetch operation fails, it displays an error message.
Now you can run your server with the following command:
$ node server.js
Server running at http://localhost:3000/
Open a web browser and navigate to http://localhost:3000/
. You should see the webpage served by your Express server. Click the "Fetch Joke" button to make a request to the API endpoint. The data (either the actual data from the API or the fallback data) will be displayed on the webpage. You can play with the defined external API endpoint by replacing it with non-existent API and your "Fetch Joke" should be still working.
This example ensures that your application continues to function and provide value to the user even when some parts of it (like an external API) fail. While this is a simple example, the principles of graceful degradation can be applied to more complex scenarios and can greatly improve the robustness and user experience of your applications.
And as always, subscribe to our newsletter and stay tuned to our blog for the other deep dives on this topic.
Member discussion