“Graceful Degradation” is a phrase that describes a process triggered by a Web client at runtime. The process enables a page to continue working even after erring. A page thereby often reverts to static content because insufficient support was detected. To grasp Graceful Degradation, one must first grasp the Web client.
There is only a trade off in accommodating ancient browsers if it is necessary to write code to actively accommodate them. If a system is being designed to facilitate [Graceful Degradation] then the work needed to accommodate the most modern browsers with [JavaScript] disabled has effectively accommodated those ancient browsers in some sense.
—Richard Cornford (on Graceful Degradation)[0]
The Web client is one of the most hostile environments that a software developer can face. Many variables can cause a page to falter. Each layer of the Web client has variables which can trigger differing behaviour in a page. They are outlined in detail below.
Contrary to what some[1] have written, “Progressive Enhancement” differs from Graceful Degradation. Progressive Enhancement is a phrase that describes a build process undertaken by a developer while building a Web page. A default state is first created with HTML; CSS is then layered on to decorate. Finally, JavaScript is added to give the page dynamic behaviour. This incremental process allows Web pages to be stable should variables change.
On the Internet, HTTP is how text (née “hypertext”) is transmitted. Colloquially, it's how resources (read: files) are transmitted. However, there are many ways that a user or client can modify transmission of resources. The following list contains some common caveats.
The Host header (specified in HTTP/1.1)[2] is specified to contain the host of the resource requested. Colloquially, one might refer to a “host” as a “domain” or “domain name”. Hosts can be blocked by Web clients through a preferences panel or the user through a hosts file. Quite often, this feature is used to block advertising hosts, and by proxy, advertising. Therefore, it is wise to anticipate an external resource being blocked (Google Analytics is a common target). Some examples would include: images, CSS stylesheets and JavaScript files. Sometimes, a client will block a host without the user's explicit consent. This can be caused by a “Web filter”, which is used by many administrators to prevent users from browsing undesired or lewd content.
The 4xx range of status codes (specified in HTTP/1.1)[3] denotes an error on the part of the Web client. Conversely, the 5xx range of status codes (also specified in HTTP/1.1)[4] denote errors on the part of the Web server. When applied to external resources in a Web page, they can encumber an entire page unless dealt with judiciously. One such example is the following JavaScript snippet:
/* NOTE: use absolute paths to avoid problems with the `src` property (absolute) not reflecting the `src` attribute (relative or absolute). */ var img = document.createElement("img"); img.onload = function (evt) { this.alt = "Image loaded successfully"; this.src = "http://localhost/success.gif"; }; img.onerror = function (evt) { this.alt = "Image could not be loaded"; // necessary to avoid infinite event loop if (img.src !== "http://localhost/error.gif") { this.src = "http://localhost/error.gif"; } }; img.alt = "Image is loading"; img.width = 250; img.height = 250; img.src = "http://localhost/placeholder.gif"; document.body.appendChild(img);
which will replace an image's src
property with an image
indicative of its current state. The alt
property is
populated with a message for each state. In the event that the error
image doesn't load, a message will still be available for the user
to read. Another example is an external script file that fails to load.
This snippet is an example:
if (typeof Library === "object" && Library) { // `Library` script was loaded. (function () { // application code here }()); }
which prevents errors caused by referencing an object (read: API)
that is undefined
. Utils uses this
pattern for both modules and unit tests.
HTML is a markup language derived from SGML that allows one to author Web content. It contains variables which can be modified by the Web client. Some examples follow.
Within an HTML (along with XHTML and XML) document, a doctype is used to notify the client of what type the document is to be parsed as. Doctypes usually contain the version of the mark-up language used along with a category (such as “transitional” or “strict”). With the entrance of HTML 5, a version number and category are no longer necessary. This snippet:
<!DOCTYPE HTML>
is a sufficient identifier of an HTML 5 document. Omission of a doctype can cause a client to parse the page in “Quirks Mode”. Quirks Mode is a parsing mode wherein a client attempts to parse a document to conform to a legacy, or outdated style.
Please don’t [design for Quirks Mode]. Willfully designing for the Quirks [Mode] will come and haunt you, your coworkers or your successors in the future [...] . Designing for the Quirks [Mode] is a bad idea. Trust me.
—Henri Sivonen (on Quirks Mode)[5]
Environments that do not recognise a particular element will render textual content inside. This behaviour can be used to leave messages to the user that the feature is unsupported. In HTML 5, a snippet such as:
<video src="eggn.ogg"> The video element is unsupported in this environment. </video>
is a good starting point. In supported environments, the textual content is not rendered.
Environments that do not recognise a particular property will ignore it. This snippet:
<input name="first_name" placeholder="First Name" type="text">
is an example wherein an unrecognised attribute
(the placeholder
attribute)
will often be ignored.
CSS is a style sheet language used to visually format HTML. Like HTML, it also contains variables which can by modified by the Web client. What follows is a set of examples.
Unrecognised properties will be ignored by an environment. This facilitates the use of newer features while having little effect on outdated environments. For example, this snippet:
.featured { background-color: #FFEE77; /* ignored in older browsers such as IE 7 */ border-radius: 1em; }
exemplifies how visual effects can be stacked to accommodate supportive environments.
Proprietary properties pertain to a specific environment. They are non-standard and are thus discouraged unless only that particular environment is desired. Quite often, they are prefixed with a prefix representing the environment to be targeted (e.g. "-ms" for Microsoft). The following snippet:
.highlighted { /* proprietary webkit property */ -webkit-text-stroke: 1px #FF0000; }
is an example of a proprietary property (specific to the Webkit engine) that is non-standard and tailored for a specific environment.
JavaScript is a scripting language and a subset of the ECMAScript language. Unfortunately, it contains many variables that can by modified by a Web client. Common problem points are outlined below.
Unrecognised native features will err upon use in an environment. As a result, newer features should be used with caution. The following snippet:
// errs in IE 8 var keys = Object.keys({});
is an example of a newer feature that will cause problems for older environments. As a result, older core features with the widest amount of support are encouraged over newer features. This snippet:
function grabKeys( obj ) { var keys = [], key; for (key in obj) { if (obj.hasOwnProperty(key)) { keys[keys.length] = key; } } return keys; }
could be used as an alternative in order to support more environments, though its behaviour deviates slightly from the native method.
Like native features, unrecognised host features will err upon use in an environment. However, host features are even more volatile as their implementation in ECMAScript is largely undefined. A common example is the Document Object Model (DOM), which is designed to be language-independent. Furthermore, host objects are very unreliable when used for “type” inferences, as their “types” vary between environments. Code such as this:
// "function" in Opera 8 // "object" in Opera 9 typeof document.childNodes;
illustrates just one inconsistency between environments. There are numerous predatory bugs lying underneath older environments, but due dilligence can defeat many of them. One example is this snippet:
// "Type Mismatch" in IE 6 xhr.onreadystatechange = null;
which can be replaced with the following snippet:
// type match xhr.onreadystatechange = function () {};
thereby removing the “type mismatch”, as a function was expected. However, some are more difficult, such as this snippet:
// "object" in IE 5 typeof document.createDocumentFragment; // Error: "Not Implemented" in IE 5 document.createDocumentFragment();
which would also be called an “uncallable function”. One solution to the preceding is to use the following snippet:
var canCallDocFrag = (function () { var result = true; try { document.createDocumentFragment(); } catch (err) { result = false; } return result; }());
which will catch the error thrown from the invocation of the specified method. An immediate closure is used to reduce repetition. A derivative of this strategy is used in Utils to complete IE 5 support.
Once one has learned of the follies that populate the Web client, one can then learn how to safeguard against them. A wide array of strategies exist to author defensively for the Web. They are explained below.
HTML does not explicitly contain tools for defensive authoring, but its status as the building blocks of a Web page allows it to be used defensively.
A default state is the first state in which a Web client is presented with upon loading a page. For HTML pages, this means static HTML. Default states are used to provide clients with forms of pages while not patronising users of these clients. Messages such as: “Please upgrade your browser; this one is insufficient” and “Please enable JavaScript to view this page” only insult the user's choice (or often a lack thereof) of environment.
An optimal approach is to provide a static alternative. For example, consider a proposal to create a rotating image slideshow for the home page of a large Web site. JavaScript would be the presumptive choice to rotate images at the behest of a timer. However, consider a scenario wherein scripting is disabled. Perhaps an administrator has disabled scripting out of security concerns. The user may be left with an empty “slide show” depending on the default state written. A defensive strategy would be to write one image directly into the HTML mark-up and provide a link to a page containing all proposed feature links in static form. Consider this snippet:
<div id="slideshow"> <a href="slides.html" id="slide_link" title="Feature Slideshow"><img alt="Feature 1" height="480" id="current_slide" name="current_slide" src="images/feature1.jpg" width="640"></a> <div>
which would then be followed by this theoretical JavaScript snippet:
var link = document.getElementById("slide_link"), slide = document.images.current_slide, slideIndex, slideTimer, maxIndex = 3; function playSlide() { slide.alt = "Feature" + slideIndex; slide.src = "images/feature" + slideIndex + ".jpg"; link.href = "http://localhost/feature" + slideIndex + ".html"; link.title = "Feature" + slideIndex; } function endShow() { clearTimeout(slideTimer); slideTimer = null; startShow(); } function runTimer() { if (slideIndex <= maxIndex) { slideTimer = setTimeout( runTimer, 3000 ); playSlide(); slideIndex += 1; } else if (slideIndex > maxIndex) { endShow(); } } function startShow() { slideIndex = 1; runTimer(); } startShow();
which is a naïve slide show that provides a default state for the user should the script fail to execute (be it a preference or a failed load).
Default styles are often overlooked by developers.
When respected, they can yield a defensive foundation
for Web pages. One such case is the <p>
element, which has a top and bottom margin specified[6] in CSS 2.1. It
can be used to easily style and separate form controls.
The following snippet is an example:
<form action="http://localhost/" id="test_form" method="post" name="test_form"> <fieldset> <legend>Enter Your Credentials</legend> <p> <label> <input name="username_control" type="text"> Username </label> </p> <p> <label> <input name="password_control" type="password"> Password </label> </p> <p> <input name="submit_control" type="submit" value="Log in"> </p> </fieldset> </form>
which eschews <table>
elements for
<p>
elements; this provides a quicker,
flexible solution.
CSS can be written defensively to minimise loopholes created by deficient environments. By testing a wide range of browsers and studying standards, one can acquire knowledge of defensive CSS strategies. Some examples follow.
Media types were first introduced[7] in CSS 2.1. They have been expanded to provide complex logical assertions, or “queries”, and are specified as a module[8] of CSS 3.
Querying for the current display width can be used as a strategy to facilitate a “responsive” page. This means that the page will adjust to differing screen widths. For example, a page that uses pseudo-columns floated next to each other may remove those floats if a certain width range is detected. The result is one singular pseudo-column, which is optimal for cramped display widths. The project Web site for Utils uses this strategy; its use is most evident on the home page. An example would be the following snippet:
@media screen and (max-width: 20em) { body { font-size: .75em; } }
which will shrink the font size to 12px if the display's maximum width is 320px (given 16px ems).
“Defensive Programming” is a phrase that describes a style of programming that makes few assumptions and tests heavily. It has been studied and espoused by denizens of the comp.lang.javascript[9] newsgroup for over a decade. Most often, the practice is used in browser scripting with JavaScript, particularly HTML DOM scripting. By programming defensively, one makes few assumptions and tests frequently. By testing frequently, one acquires knowledge of environmental “quirks”, and how to avoid them.
There's lots of [browsers with unknown bugs] out there. You can't know all of their quirks and you can't code for all of their quirks. All you can do is follow standards, use sound practices, keep things simple as possible and test on as many agents as is feasible.
—David Mark (on Defensive Programming)[10]
isHostMethod
“isHostMethod” is a method that has been
referenced[11]
in the comp.lang.javascript newsgroup innumerable
times. It searches for a typeof
result of
"function"
, "object"
or
"unknown"
. The first two have been covered
in an example earlier. If the specified property matches
either of those two,
it must be “truthy”
to pass. The final “type”
refers to a special case wherein ActiveX objects
will return the preceding “type” in
earlier versions of Internet Explorer. It cannot
have its “truthiness”
examined, as doing so can throw an error. Utils
uses a derivative of isHostMethod
called
“isHostObject”. isHostMethod
's seminal
form is the following:
function isHostMethod(o, m) { var t = typeof o[m]; return (/^(function|object)$/.test(t) && o[m]) || t == "unknown"; }
which is a snippet from David Mark's “My Library”[12].
isHostMethod
was derived from a function created
by Thomas Lahn, entitled “isMethodType”.
Two different environments can, and probably will produce two different results. Instead of striving for a multiplicity of identical results, strive for a multiplicity of acceptable results that present a user with a respectable interface regardless of the environment.
Earlier, code for an example “slideshow” was written. However, it doesn't quite gracefully degrade. Using Utils, Graceful Degradation can be achieved. Here is a revised version:
var global = global || this; if (typeof Utils === "object" && Utils) { (function () { var commonElements, slideIndex, slideTimer, maxIndex = 3; function grabById( id ) { var result = null; // method is part of a "dynamic interface" if (Utils.select.byId) { result = Utils.select.byId( global.document, id ); } return result; } function grabNamedImage( name ) { var result = null; // method is part of a "dynamic interface" if (Utils.select.images) { result = Utils.select.images( global.document, name ); } return result; } commonElements = (function () { var result = {}; result.link = grabById("slide_link"); result.slide = grabNamedImage("current_slide"); return result; }()); function playSlide() { var slide = commonElements.slide, link = commonElements.link; if (slide && link) { slide.alt = "Feature" + slideIndex; slide.src = "images/feature" + slideIndex + ".jpg"; link.href = "http://localhost/feature" + slideIndex + ".html"; link.title = "Feature" + slideIndex; } } function removeTimeout( ref ) { var key = "clearTimeout", result = null; if (Utils.is.hostObject(global[key])) { result = global[key]( ref ); } return result; } function endShow() { removeTimeout(slideTimer); slideTimer = null; startShow(); } function makeTimeout( callback, time ) { var key = "setTimeout", result = null; if (Utils.is.hostObject(global[key])) { result = global[key]( callback, time ); } return result; } function runTimer() { var timeout; if (slideIndex <= maxIndex) { timeout = makeTimeout( runTimer, 3000 ); if (timeout) { slideTimer = timeout; playSlide(); slideIndex += 1; } } else if (slideIndex > maxIndex) { endShow(); } timeout = null; } function startShow() { slideIndex = 1; runTimer(); } startShow(); }()); }
which is far more defensive and degrades cleanly. Note the use of wrapper functions and result checks, enabling a more defensive approach.
The preceding code is used in a functional demo[13].
for viewing. It has been tested successfully and degrades gracefully in IE 4+.