When JavaScript Feature Detection Fails

Share this article

Once upon a time, browser detection was the stock-in-trade of JavaScript programmers. If we knew that something worked in IE5 but not in Netscape 4, we’d test for that browser and fork the code accordingly. Something like this:

if(navigator.userAgent.indexOf('MSIE 5') != -1)
{
  //we think this browser is IE5
}

But the arms-race was already well underway when I first joined this industry! Vendors were adding extra values to the user-agent string, so they’d appear to be their competitor’s browser, as well as their own. For example, this is Safari 5 for Mac:

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.59.10 (KHTML, like Gecko) Version/5.1.9 Safari/534.59.10

That will match tests for Safari and Webkit as well as KHTML (the Konqueror codebase that Webkit is based on); but it also matches Gecko (which is Firefox’s rendering engine), and of course Mozilla (because almost every browser claims to be Mozilla, for historical reasons).

The purpose of adding all these values is to circumvent browser detection. If a script assumes that only Firefox can handle a particular function, it might otherwise exclude Safari, even though it would probably work. And don’t forget that users themselves can change their user-agent — I’ve been known to set my browser to identify as Googlebot/1.0, so I can access content the site-owner thinks is only available for crawling!

So over time, this kind of browser detection has become an impossible tangle, and has largely fallen out of use, to be superseded by something far better — feature detection.

Feature detection simply tests for the features we want to use. For example, if we need getBoundingClientRect (to get the position of an element relative to the viewport), then the important thing is whether the browser supports it, not what browser that is; so rather than testing for supported browsers, we test for the feature itself:

if(typeof document.documentElement.getBoundingClientRect != "undefined")
{
  //the browser supports this function
}

Browsers which don’t support that function will return a type of "undefined", and therefore won’t pass the condition. Without us having to test the script in any specific browser, we know it will either work correctly, or silently fail.

Or do we …?

But here’s the thing — feature detection isn’t completely reliable either — there are times where it fails. So let’s take a look at some examples now, and see what we can do to solve each case.

The ActiveX object

Perhaps the most famous example of where feature detection fails, is testing for ActiveXObject to make an Ajax request in Internet Explorer.

ActiveX is an example of a late binding object, the practical meaning of which is that you can’t know whether it will be supported until you try to use it. So code like this will throw an error if the user has ActiveX disabled:

if(typeof window.ActiveXObject != "undefined")
{
  var request = new ActiveXObject("Microsoft.XMLHTTP");
}

To solve this problem we need to use exception handlingtry to instantiate the object, catch any failure, and deal with it accordingly:

if(typeof window.ActiveXObject != "undefined")
{
  try
  {
    var request = new ActiveXObject("Microsoft.XMLHTTP");
  }
  catch(ex)
  {
    request = null;
  }
  if(request !== null)
  {
    //... we have a request object
  }
}

HTML attributes mapped to DOM properties

Property mappings are often used to test support for the API that goes with an HTML5 attribute. For example, checking that an element with [draggable="true"] supports the Drag and Drop API, by looking for the draggable property:

if("draggable" in element)
{
  //the browser supports drag and drop
}

The problem here is that IE8 or earlier automatically maps all HTML attributes to DOM properties. This is why getAttribute is such a mess in these older versions, because it doesn’t return an attribute at all, it returns a DOM property.

This means that if we use an element which already has the attribute:

<div draggable="true"> ... </div>

Then the draggable test will return true in IE8 or earlier, even though they don’t support it.

The attribute could be anything:

<div nonsense="true"> ... </div>

But the result will be the same — IE8 or earlier will return true for ("nonsense" in element).

The solution in this case is to test with an element which doesn’t have the attribute, and the safest way to do that is to use a created element:

if("draggable" in document.createElement("div"))
{
  //the browser really supports drag and drop
}

Assumptions about user behaviour

You might have seen code like this used to detect touch devices:

if("ontouchstart" in window)
{
  //this is a touch device
}

Most touch devices implement an artificial delay before firing click events (usually around 300ms), which is so that elements can be double-tapped without clicking them as well. But this can make an application feel sluggish and unresponsive, so developers sometimes fork events using that feature test:

if("ontouchstart" in window)
{
  element.addEventListener("touchstart", doSomething);
}
else
{
  element.addEventListener("click", doSomething);
}

However this condition proceeds from a false assumption — that because a device supports touch, therefore touch will be used. But what about touch-screen laptops? The user might be touching the screen, or they might be using a mouse or trackpad; the code above can’t handle that, so clicking with the mouse would do nothing at all.

The solution in this case is not to test for event support at all — instead, bind both events at once, and then use preventDefault to stop the touch from generating a click:

element.addEventListener("touchstart", function(e)
{
  doSomething();
  
  e.preventDefault();
  	
}, false);
  
element.addEventListener("click", function()
{
  doSomething();
  
}, false);

Stuff that just plain doesn’t work

It’s a painful thing to concede, but sometimes it isn’t the feature we need to test for — it’s the browser — because a particular browser claims support for something that doesn’t work. A recent example of this is setDragImage() in Opera 12 (which is a method of the drag and drop dataTransfer object).

Feature testing fails here because Opera 12 claims to support it; exception handling won’t help either, because it doesn’t throw any errors. It just plain doesn’t work:

//Opera 12 passes this condition, but the function does nothing
if("setDragImage" in e.dataTransfer)
{
  e.dataTransfer.setDragImage("ghost.png", -10, -10);
}

Now that might be fine if all you want is to try adding a custom drag image, and are happy to leave the default if that’s not supported (which is what will happen). But what if your application really needs a custom image, to the extent that browsers which don’t support it should be given an entirely different implementation (i.e. using custom JavaScript to implement all the drag behaviors)?

Or what if a browser implements something, but with rendering bugs that can’t be prevented? Sometimes we have no choice but to explicitly detect the browser in question, and exclude it from using a feature it would otherwise try to support.

So the question becomes — what’s the safest way to implement browser detection?

I have two recommendations:

  1. Use proprietary object tests in preference to navigator information.
  2. Use it for excluding browsers rather than including them.

For example, Opera 12 or earlier can be detected with the window.opera object, so we could test for draggable support with that exclusion:

if(!window.opera && ("draggable" in document.createElement("div")))
{
  //the browser supports drag and drop but is not Opera 12
}

It’s better to use proprietary objects rather than standard ones, because the test result is less like to change when a new browser is released. Here are some of my favourite examples:

if(window.opera)
{
  //Opera 12 or earlier, but not Opera 15 or later
}
if(document.uniqueID)
{
  //any version of Internet Explorer
}
if(window.InstallTrigger)
{
  //any version of Firefox
}

Object tests can also be combined with feature testing, to establish support for a particular feature within a specific browser, or at a pinch, to define more precise browser conditions:

if(document.uniqueID && window.JSON)
{
  //IE with JSON (which is IE8 or later)
}
if(document.uniqueID && !window.Intl)
{
  //IE without the Internationalization API (which is IE10 or earlier)
}

We’ve already noted how the userAgent string is an unreliable mess, but the vendor string is actually quite predictable, and can be used to reliably test for Chrome or Safari:

if(navigator.vendor == 'Google Inc.')
{
  //any version of Chrome
}
if(navigator.vendor == 'Apple Computer, Inc.')
{
  //any version of Safari (including iOS builds)
}

The golden rule with all of this is to be extremely careful. Make sure you test conditions in as many browsers as you can run, and think about them carefully in terms of forward compatibility — aim to use browser conditions for excluding browsers because of a known bug, rather than including them because of a known feature (which is what feature testing is for)

And fundamentally, always start by assuming full compliance with feature testing — assume that a feature will work as expected unless you know otherwise.

Choosing the test syntax

Before we go, I’d like to examine the different kinds of syntax we can use for object and feature tests. For example, the following syntax has become common in recent years:

if("foo" in bar)
{
}

We couldn’t use that in the past because IE5 and its contemporaries threw an error over the syntax; but that’s no longer an issue now that we don’t have to support those browsers.

In essence, it amounts to exactly the same as this, but is shorter to write:

if(typeof bar.foo != "undefined")
{
}

However test conditions are often written with reliance on automatic type conversion:

if(foo.bar)
{
}

We used that syntax earlier in some of the browser object tests (such as the test for window.opera), and that was safe because of how objects evaluate — any defined object or function will always evaluate to true, whereas if it were undefined it would evaluate to false.

But we might be testing something that validly returns null or empty-string, both of which evaluate to false. For example, the style.maxWidth property is sometimes used to exclude IE6:

if(typeof document.documentElement.style.maxWidth != "undefined")
{
}

The maxWidth property only evaluates to true if it’s supported and has an author-defined value, so if we wrote the test like this, it might fail:

if(document.documentElement.style.maxWidth)
{
}

The general rule is this: reliance on automatic type conversion is safe for objects and functions, but is not necessarily safe for strings and numbers, or values which might be null.

Having said that — if you can safely use it, then do so, because it’s usually much faster in modern browsers (presumably because they’re optimized for exactly that kind of condition).

For more about this, see: Automatic Type Conversion In The Real World.

Frequently Asked Questions about JavaScript Feature Detection

What is JavaScript feature detection and why is it important?

JavaScript feature detection is a technique used by developers to determine if a specific feature or API is supported in a user’s browser. This is crucial because not all browsers support all features of JavaScript. By using feature detection, developers can provide alternative solutions or fallbacks for unsupported features, ensuring that the website or application functions correctly across different browsers. This enhances user experience and ensures compatibility.

How does JavaScript feature detection fail?

JavaScript feature detection can fail due to several reasons. One common reason is the incorrect implementation of the feature detection code. For instance, if the code checks for a property that doesn’t exist in the object, it will return undefined, leading to a false negative. Another reason could be the browser’s quirks or bugs, which may cause the feature detection to give inaccurate results.

What is the difference between feature detection and browser detection?

Feature detection involves checking if a specific feature or API is supported in a user’s browser, while browser detection identifies the user’s browser and version. While both techniques aim to ensure compatibility and functionality, feature detection is generally considered a better practice because it directly checks for the feature rather than assuming its support based on the browser type or version.

How can I use JavaScript to detect a mobile device?

You can use the navigator.userAgent property in JavaScript to detect a mobile device. This property returns a string that represents the browser’s user-agent header. By checking for specific keywords in this string, such as ‘Android’, ‘iPhone’, or ‘iPad’, you can determine if the user is on a mobile device.

What is Feature.js and how does it help in feature detection?

Feature.js is a lightweight, fast, and simple JavaScript utility for feature detection. It offers an easy-to-use API that allows developers to test if a specific feature is supported in the browser. This can help in providing fallbacks or alternative solutions for unsupported features, enhancing the compatibility and functionality of your website or application.

What is Modernizr and how does it aid in feature detection?

Modernizr is a JavaScript library that helps developers take advantage of HTML5 and CSS3 features while maintaining compatibility with older browsers. It uses feature detection to check if a specific feature is supported in the browser, and adds classes to the HTML element that allow you to target specific browser functionality in your stylesheet or JavaScript.

How can I use the device-detector-js package for feature detection?

The device-detector-js package is a powerful tool for device detection. It parses user-agent strings and detects devices like smartphones, tablets, desktops, TVs, and more. It also detects the browser, engine, OS, and other useful information. You can use this package to tailor your website or application’s behavior based on the detected device.

What are some best practices for implementing feature detection?

Some best practices for implementing feature detection include: using reliable and tested libraries like Modernizr or Feature.js, testing your feature detection code thoroughly across different browsers and devices, providing alternative solutions or fallbacks for unsupported features, and avoiding assumptions about feature support based on the browser type or version.

Can feature detection help improve website performance?

Yes, feature detection can help improve website performance. By detecting unsupported features and providing alternative solutions or fallbacks, you can prevent unnecessary code from running in the browser. This can reduce the load time and improve the overall performance of your website.

How can I stay updated with the latest features supported by different browsers?

Staying updated with the latest features supported by different browsers can be challenging due to the rapid pace of web development. However, resources like the Mozilla Developer Network (MDN), Can I Use, and the JavaScript documentation can provide up-to-date information about feature support across different browsers.

James EdwardsJames Edwards
View Author

James is a freelance web developer based in the UK, specialising in JavaScript application development and building accessible websites. With more than a decade's professional experience, he is a published author, a frequent blogger and speaker, and an outspoken advocate of standards-based development.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week