Chapter 12. Browser Detection

Browser detection is always a hot-button topic in web development. This battle predates JavaScript browser detection by a couple of years and begins with the introduction of Netscape Navigator, the first truly popular and widely used web browser. Netscape Navigator 2.0 was so far beyond any of the other available web browsers that websites began looking for its specific user-agent string before returning any useful content. This forced other browser vendors, notably Microsoft, to include things in their user-agent string to get around this form of browser detection.

User-Agent Detection

The earliest form of browser detection was user-agent detection, a process by which the server (and later the client) looked at the user-agent string and determined the browser. During that time, servers would regularly block certain browsers from viewing anything on the site based solely on the user-agent string. The browser that benefited the most was Netscape Navigator. Netscape was certainly the most capable browser, so websites targeted that browser as the only one that could properly display the site. Netscape’s user-agent string looked like this:

Mozilla/2.0 (Win95; I)

When Internet Explorer was first released, it was essentially forced to duplicate a large part of the Netscape user-agent string to ensure that servers would serve up the site to this new browser. Because most user-agent detection was done by searching for “Mozilla” and then taking the version number after the slash, the Internet Explorer user-agent string was:

Mozilla/2.0 (compatible; MSIE 3.0; Windows 95)

Internet Explorer’s introduction meant that everyone’s user-agent string detection identified Internet Explorer as Netscape. This started a trend of new browsers partially copying the user-agent strings of existing browsers that continued up through the release of Chrome, whose user-agent string contains parts of Safari’s string, which in turn contained parts of Firefox’s string, which in turn contained parts of Netscape’s string.

Fast forward to the year 2005, when JavaScript started to increase in popularity. The browser’s user-agent string, the same one reported to the server, is accessible in JavaScript through navigator.userAgent. User-agent string detection moved into web pages with JavaScript performing the same type of user-agent string detection as the server, such as:

// Bad
if (navigator.userAgent.indexOf("MSIE") > -1) {
    // it's Internet Explorer
} else {
    // it's not
}

As more websites took to user-agent detection in JavaScript, a new group of sites started to fail in browsers. The same problem for bit servers nearly a decade earlier had reemerged in the form of JavaScript.

The big problem is that user-agent string parsing is difficult, due to the way browsers have copied one another to try to ensure compatibility. With every new browser, user-agent detection code needs to be updated, and the time between the browser is released to the time the changed code is deployed could mean untold numbers of people getting a bad user experience.

This isn’t to say that there isn’t any way to use the user-agent string effectively. There are well-written libraries, both in JavaScript and for the server, that provide a reasonably good detection mechanism. Unfortunately, these libraries also require constant updates as browsers continue to evolve and new browsers are released. The overall approach isn’t maintainable over the long term.

User-agent detection should always be the last approach to determining the correct course of action in JavaScript. If you choose user-agent detection, then the safest way to proceed is by detecting only older browsers. For instance, if you need to do something special to make your code work in Internet Explorer 8 and earlier, then you should detect Internet Explorer 8 and earlier rather than trying to detect Internet Explorer 9 and higher, such as:

if (isInternetExplorer8OrEarlier) {
    // handle IE8 and earlier
} else {
    // handle all other browsers
}

The advantage you have in this situation is that the Internet Explorer 8 and earlier user-agent strings are well known and won’t be changing. Even if your code continues to run through the release of Internet Explorer 25, the code will most likely continue to work without further modifications. The opposite isn’t true—you’ll be stuck updating code constantly if you try to detect Internet Explorer 9 and higher.

Note

Browsers don’t always report their original user-agent string. User-agent switchers are readily available for nearly all web browsers. Developers are frequently concerned about this and therefore won’t turn to user-agent detection, even when it’s the only option, because “you can never know for sure.” My advice is not to worry about user-agent spoofing. If a user is savvy enough to switch her user-agent string, then she’s also savvy enough to understand that doing so might cause websites to break in unforeseen ways. If the browser identifies itself as Firefox and doesn’t act like Firefox, that’s not your fault. There’s no point in trying to second-guess the reported user-agent string.

Feature Detection

Looking to use a more sane approach to browser-based conditionals, developers turned to a technique called feature detection. Feature detection works by testing for a specific browser feature and using it only if present. So instead of doing something like this:

// Bad
if (navigator.userAgent.indexOf("MSIE 7") > -1) {
    // do something
}

you should do something like this:

// Good
if (document.getElementById) {
    // do something
}

There is a distinction between these two approaches. The first is testing for a specific browser by name and version; the second is testing for a specific feature, namely document.getElementById. So user-agent sniffing results in knowing the exact browser and version being used (or at least the one being reported by the browser) and feature detection determines whether a given object or method is available. Note that these are two completely different results.

Because feature detection doesn’t rely on knowledge of which browser is being used, only on which features are available, it is trivial to ensure support in new browsers. For instance, when the DOM was young, not all browsers supported document.getElementById(), so there was a lot of code that looked like this:

// Good
function getById (id) {

    var element = null;

    if (document.getElementById) {    // DOM
        element = document.getElementById(id);
    } else if (document.all) {      // IE
        element = document.all[id];
    } else if (document.layers) {    // Netscape <= 4
        element = document.layers[id];
    }

    return element;
}

This is a good and appropriate use of feature detection, because the code tests for a feature and then, if it’s there, uses it. The test for document.getElementById() comes first because it is the standards-based solution. After that come the two browser-specific solutions. If none of these features is available, then the method simply returns null. The best part about this function is that when Internet Explorer 5 and Netscape 6 were released with support for document.getElementById(), this code didn’t need to change.

The previous example illustrates several important parts of good feature detection:

  1. Test for the standard solution

  2. Test for browser-specific solutions

  3. Provide a logical fallback if no solution is available

The same approach is used today with the cutting-edge features that browsers have implemented experimentally while the specification is being finalized. For instance, the requestAnimationFrame() method was being finalized toward the end of 2011, at which time several browsers had already implemented their own version with a vendor prefix. The proper feature detection for requestAnimationFrame() looks like this:

// Good
function setAnimation (callback) {

    if (window.requestAnimationFrame) {                 // standard
        return requestAnimationFrame(callback);
    } else if (window.mozRequestAnimationFrame) {       // Firefox
        return mozRequestAnimationFrame(callback);
    } else if (window.webkitRequestAnimationFrame) {    // WebKit
        return webkitRequestAnimationFrame(callback);
    } else if (window.oRequestAnimationFrame) {         // Opera
        return oRequestAnimationFrame(callback);
    } else if (window.msRequestAnimationFrame) {        // IE
        return msRequestAnimationFrame(callback);
    } else {
        return setTimeout(callback, 0);
    }

}

This code starts by looking for the standard requestAnimationFrame() method, and only if it’s not found does it continue to look for the browser-specific implementation. The very last option, for browsers with no support, is to use setTimeout() instead. Once again, this code won’t need to be updated even after the browsers have switched to using a standards-based implementation.

Avoid Feature Inference

One inappropriate use of feature detection is called feature inference. Feature inference attempts to use multiple features after validating the presence of only one. The presence of one feature is inferred by the presence of another. The problem is, of course, that inference is an assumption rather than a fact, and that can lead to maintenance issues. For example, here’s some older code using feature inference:

// Bad - uses feature inference
function getById (id) {

    var element = null;

    if (document.getElementsByTagName) {    // DOM
        element = document.getElementById(id);
    } else if (window.ActiveXObject) {      // IE
        element = document.all[id];
    } else {                                // Netscape <= 4
        element = document.layers[id];
    }

    return element;
}

This function is feature inference at its worst. There are several inferences being made:

  • If document.getElementsByTagName() is present, then document.getElementById() is present. In essence, this assumption is inferring from the presence of one DOM method that all DOM methods are available.

  • If window.ActiveXObject is present, then document.all is present. This inference basically says that window.ActiveXObject is present only for Internet Explorer, and document.all is also present only in Internet Explorer, so if you know one is there then the other must also be there. In fact, some versions of Opera supported document.all.

  • If neither of these inferences is true, then it must be Netscape Navigator 4 or earlier. This isn’t strictly true.

You cannot infer the existence of one feature based on the existence of another feature. The relationship between two features is tenuous at best and circumstantial at worst. It’s like saying, “If it looks like a duck, then it must quack like a duck.”

Avoid Browser Inference

Somewhere along the lines, a lot of web developers grew confused about the distinction between user-agent detection and feature detection. Code started being written similar to this:

// Bad
if (document.all) {  // IE
    id = document.uniqueID;
} else {
    id = Math.random();
}

The problem with this code is that a test for document.all is used as an implicit check for Internet Explorer. Once it’s known that the browser is Internet Explorer, the assumption is that it’s safe to use document.uniqueID, which is IE-specific. However, all you tested was whether document.all is present, not whether the browser is Internet Explorer. Just because document.all is present doesn’t mean that document.uniqueID is also available. There’s a false implication that can cause the code to break.

As a clearer statement of this problem, developers started replacing code like this:

var isIE = navigator.userAgent.indexOf("MSIE") > -1;

With code like this:

// Bad
var isIE = !!document.all;

Making this change indicates a misunderstanding of “don’t use user-agent detection.” Instead of looking for a particular browser, you’re looking for a feature and then trying to infer that it’s a specific browser, which is just as bad. This is called browser inference and is a very bad practice.

Somewhere along the line, developers realized that document.all was not, in fact, the best way to determine whether a browser was Internet Explorer. The previous code was replaced with more specific code, such as this:

// Bad
var isIE = !!document.all && document.uniqueID;

This approach falls into the “too clever” category of programming. You’re trying too hard to identify something by describing an increasing number of identifying aspects. What’s worse is that there’s nothing preventing other browsers from implementing the same capabilities, which will ultimately make this code return unreliable results.

Browser inference even made it into some JavaScript libraries. The following snippet comes from MooTools 1.1.2:

// from MooTools 1.1.2
if (window.ActiveXObject) 
    window.ie = window[window.XMLHttpRequest ? 'ie7' : 'ie6'] = true;
else if (document.childNodes && !document.all && !navigator.taintEnabled) 
    window.webkit = window[window.xpath ? 'webkit420' : 'webkit419'] = true;
else if (document.getBoxObjectFor != null || window.mozInnerScreenX != null) 
    window.gecko = true;

This code tries to determine which browser is being used based on browser inference. There are several problems with this code:

  • Internet Explorer 8, which supports both window.ActiveXObject and window.XMLHttpRequest, will be identified as Internet Explorer 7.

  • Any browser that implements document.childNodes is likely to be reported as WebKit if it’s not already identified as Internet Explorer.

  • The number of WebKit versions being identified is far too small, and once again, WebKit 422 and higher will be incorrectly reported as WebKit 422.

  • There is no check for Opera, so either Opera will be incorrectly reported as one of the other browsers, or it won’t be detected at all.

  • This code will need to be updated whenever a new browser is released.

The number of issues with the browser inference code is quite daunting, especially the last one. For every new browser release, MooTools would have had to update this code and get it pushed out to all users quite quickly to avoid code breaking. That’s just not maintainable in the long run.

To understand why browser inference doesn’t work, you need only look back to high school math class, in which logic statements are typically taught. Logic statements are made up of a hypothesis (p) and a conclusion (q) in the form “if p, then q.” You can try altering the statement form to determine truths. There are three ways to alter the statement:

  • Converse: if q, then p

  • Inverse: if not p, then not q

  • Contrapositive: if not q, then not p

There are two important relationships among the various forms of the statement. If the original statement is true, then the contrapositive is also true. For example, if the original statement was “If it’s a car, then it has wheels” (which is true) then the contrapositive, “if it doesn’t have wheels, then it’s not a car,” is also true.

The second relationship is between the converse and the inverse, so if one is true, then the other must also be true. This makes sense logically, because the relationship between converse and inverse is the same as between original and contrapositive.

Perhaps more important than these two relationships are the relationships that don’t exist. If the original statement is true, then there is no guarantee that the converse is true. This is where feature-based browser detection falls apart. Consider this true statement: “If it’s Internet Explorer, then document.all is implemented.” The contrapositive, “If document.all is not implemented, then it’s not Internet Explorer,” is also true. The converse, “If document.all is implemented, then it’s Internet Explorer,” is not strictly true (some versions of Opera implemented it). Feature-based detection assumes that the converse is always true when, in fact, there is no such relationship.

Adding more parts to the conclusion doesn’t help, either. Consider once again the statement, “If it’s a car, then it has wheels.” The converse is obviously false: “If it has wheels, then it’s a car.” You could try making it more precise: “If it’s a car, then it has wheels and requires fuel.” Check the converse: “If it has wheels and requires fuel, then it’s a car.” Also not true, because an airplane fits that description. So try again: “If it’s a car, then it has wheels, requires fuel, and uses two axles.” Once again, the converse isn’t going to be true.

The problem is fundamental to human language: it’s very hard to use a collection of singular aspects to define the whole. We have the word “car” because it implies a lot of aspects that you would otherwise have to list to identify that thing in which you drive to work. Trying to identify a browser by naming more and more features is the exact same problem. You’ll get close, but it will never be a reliable categorization.

MooTools backed themselves, and their users, into a corner by opting for feature-based browser detection. Mozilla had warned since Firefox 3 that the getBoxObjectFor() method was deprecated and would be removed in a future release. Because MooTools relied on this method to determine whether a browser is Gecko-based, Mozilla’s removal of the method in Firefox 3.6 meant that anyone running older versions of MooTools could have code that was now affected. This situation prompted MooTools to issue a call to upgrade to the most recent version, in which the issue is “fixed.” The explanation:

We have overhauled our browser detection to be based on the user agent string. This has become the standard practice among JavaScript libraries because of potential issues, as Firefox 3.6 demonstrates. As browsers grow closer together, looking at features to separate them will become more difficult and risky. From this point forward, browser detection will only be used where it would be impossible not to, in order to give the consistent experience across browsers that one would expect from a world-class JavaScript framework.

What Should You Use?

Feature inference and browser inference are very bad practices that should be avoided at all costs. Straight feature detection is a best practice, and in almost every case, is exactly what you’ll need. Typically, you just need to know if a feature is implemented before using it. Don’t try to infer relationships between features, because you’ll end up with false positives or false negatives.

I won’t go so far as to say never use user-agent detection, because I do believe there are valid use cases. I don’t believe, however, that there are a lot of valid use cases. If you’re thinking about user-agent sniffing, keep this in mind: the only safe way to do so is to target older versions of a specific browser. You should never target the most current browser version or future versions.

My recommendation is to use feature detection whenever possible. If it’s not possible, then fall back to user-agent detection. Never, ever use browser inference, because you’ll be stuck with code that isn’t maintainable and will constantly require updating as browsers continue to evolve.