Friday 15 June 2018

Common `fetch` errors

The new fetch API is a great improvement on XMLHttpRequest but it does have a very surprising behavior that catches people out a lot. Separately, because it uses promises, a lot of people using it fall into a common promise-related error as well. This post is a quick note about both of those.

Let's say we have an endpoint that returns JSON. This is the common code you see for consuming that:

function showStuff() {
    fetch("/get/stuff")
        .then(response => response.json())
        .then(data => {
            // ...show the stuff from `data`
        });
}

There are two significant errors in the above, can you see them?

Right! They are:

  1. fetch only rejects on network errors; HTTP-level errors (like 404s or 500s) are still resolutions, but the above assumes that resolution = all is good.
  2. Rejections aren't handled at all.

So if the server is overloaded or some such, instead of handling it gracefully, the code will just fail as of the result.json() call since what it gets back is probably an HTML page for a 500 error, not JSON. And then it completely fails to handle that error, resulting in an "Unhandled rejection" error in the console.

I'm going to take these one at a time because they are completely distinct issues.

To fix #1, we need to check for response.ok, which is set for any response where the HTTP status code is 200-299 ("success"). The simple version is something like this:

function showStuff() {
    fetch("/get/stuff")
        .then(response => {
            if (!response.ok) {
                throw new Error("Failed with HTTP code " + response.status);
            }
            return response;
        })
        .then(result => result.json())
        .then(data => {
            // ...show the stuff from `data`
        });
}

There I've turned the resolution with a non-ok status into a rejection by throwing in the then handler. You may choose to do something different, but making it a rejection means all my following logic can just work on the basis that things were okay.

If, like me, you always want to do that, you might give yourself a helper function (but you may want something slightly more elaborate, read on):

function myFetch(...args) {
    return fetch(...args).then(response => {
        if (!response.ok) {
            throw new Error("Failed with HTTP code " + response.status);
        }
        return response;
    });
}

Now I can use myFetch and know that resolutions are for completely successful calls. There are lots of ways one might vary that, but that's the basic idea. Now our example is:

function showStuff() {
    myFetch("/get/stuff")
        .then(result => result.json())
        .then(data => {
            // ...show the stuff from `data`
        });
}

That's #1 sorted. But the second problem remains.

For #2, we need to follow the promise commandment:

Handle errors, or return the promise to something that does.
(I don't know if anyone actually said that — well, anyone but me. :-) ) In our example situation, it appears that showStuff is meant to be fairly standalone, so we'd handle errors inline:

function showStuff() {
    myFetch("/get/stuff")
        .then(result => result.json())
        .then(data => {
            // ...show the stuff from `data`
        })
        .catch(err => {
            // ...do something with the error that occurred
        });
}

But it would also be perfectly valid to make showStuff return the promise. In that case, we probably wouldn't call it showStuff, but perhaps getFormattedStuff on the assumption the caller will either handle the error or use the formatted result:

function showStuff() {
    return myFetch("/get/stuff")
        .then(result => result.json())
        .then(data => {
            // ...format the stuff from `data`
            return formattedStuff;
        });
}

Now it's up to the caller to handle things, both resolution (with formatted stuff) or rejection.

If, like me, you want to have a utility wrapper so you don't always have to write that response.ok logic every time. I also usually know in advance what I'm going to do with the response body (response.json(), response.text(), etc.). To make the wrapper maximally useful, we want to throw an error (because rejections should always be Errors) but also include the response, because it's perfectly valid to want to read the body of an error response. To provide that, I have an Error subclass:

class FetchError extends Error {
    constructor(response) {
        super(`HTTP error ${response.status}`);
        this.response = response;
    }
}

Then my wrappers use it:

function fetchJSON(...args) {
    return fetch(...args)
    .then(response => {
        if (!response.ok) {
            throw new FetchError(response);
        }
        return response.json();
    });
}

function fetchText(...args) {
    return fetch(...args)
    .then(response => {
        if (!response.ok) {
            throw new FetchError(response);
        }
        return response.text();
    });
}

// ...

That way, if the code using the wrapper needs to read the body of the error response, it can by using the response property on the FetchError instance.

Wednesday 16 September 2015

Automatic Semicolon Insertion

I've been recommending to people that they not rely on Automatic Semicolon Insertion for years now, and frankly wish that the strict variant had removed it. Just wanted to share the best argument for putting semicolons where they belong that I've seen in a long time:

var foo = {
    a: "ay",
    b: "bee"
}

(function() {
    console.log("Hi there");
})();

That blows up, because the opening ( in the inline-invoked function expression at the end is interpreted as the opening parentheses of a function call, with the {...} being what it tries to call. Since that's not a function, it fails with TypeError: (intermediate value)(intermediate value) is not a function.

As the lady said: Ain't nobody got time for that! Use your semicolons.

Tuesday 29 July 2014

JavaScript's Date/Time Format Is Not ISO-8601

TL;DR JavaScript's date/time string format isn't ISO-8601. It's much simpler, covers fewer use cases, and assumes strings written without a timezone indicator are in UTC (whereas ISO-8601 assumes they're "local" time).

Update October 2014: The draft ES6 specification has changed the timezone thing so ES6 will match ISO-8601 and assume local time, and some implementations (such as V8 in Chrome) have already updated their handling to implement that change.

Details:

There's a rumor going around that the date/time format in ECMAScript 5th edition (ES5, the current version of "JavaScript") is ISO-8601. It isn't. It's a bit like it, but it isn't, and despite the spec calling it a "simplification" of ISO-8601, it's not just a subset, either; the same string can mean two different times (or even years) when interpreted in ISO-8601's format vs. JavaScript's format.

Details:

ISO-8601 is incredibly complicated; JavaScript's format is much simpler

ISO-8601 covers a very wide range of use cases, not just date/times, but spans of time, various ways of writing time, and so forth. JavaScript's format is much simpler:

  • 201405 is a valid ISO-8601 string: It refers to the entire month of May 2014. It's an invalid date/time string in JavaScript.
  • 2014-05 also refers to the entire month of May in ISO-8601; in JavaScript, it refers to May 1st at midnight UTC (omitted fields have the implied value of 0).
  • ISO-8601 has week numbers, and Here There Be Dragons: 2009-W01-1 is Monday 29 December 2008, for instance. Fortunately, JavaScript doesn't do week numbers.
  • ISO-8601 allows fractions (of any length) on the smallest time unit in the string, so 2014-07-29T02.5Z is 2:30 in the morning UTC (so is 2014-07-29T02,5Z). JavaScript doesn't allow fractions, but uses the . (only, not the ,) to separate seconds from an optional milliseconds value (which is very like allowing up to three digits of a fraction on seconds).
  • And so on. ISO-8601 covers various other kinds of time spans and all kinds of things.

So yeah, not the same thing. Why is JavaScript's so different? Probably a combination of the fact that A) ISO-8601 is difficult to parse, and B) JavaScript's Date object has no way of representing a span of time (the entire month of May, for instance).

Time Zone Differences

Update (October 2014): See above, ES6 fixes this. I guess they decided it was a bug after all.

In New York, in ISO-8601, 2014-07-29T02:37:21 means 2:37 a.m. on the 29th; but in JavaScript, it means 10:37 p.m. on the 28th. That's because the string doesn't have any timezone indicator on it. ISO-8601 says that no timezone indicator means "local time," and so the string becomes context-sensitive. JavaScript says no timezone indicator implies Z (UTC). This is a pretty big difference. 2014-01-01T01:28:55 isn't even in the same year in the two systems in New York (or about half the rest of the planet).

Why would TC-39 (the committee that steers ECMAScript) do something so bone-headed? It probably wasn't bone-headed. Remember that this didn't come in until ECMAScript 5th edition, in 2009. Most likely, engines had been parsing strings like YYYY-MM-DD in UTC for years. TC-39 couldn't just waive a wand over it and say "Thou shalt now use local time." Too much existing code would break.

My only quarrel is with the wording in the specification, which says that the format is a "simplification" of ISO-8601. "Simplification" (to me) implies "subset," as in, a string that's valid in the subset will mean the same thing in the superset. But that's not true here. They probably should have just said "It looks a bit like ISO-8601, but it isn't" and highlighted the more important differences.

Happy coding!

Sunday 1 June 2014

VirtualBox -- USB not working

Just a snippet today, as much to remember it as anything else:

If you're using VirtualBox on Linux and USB devices aren't working despite the fact you've installed the extension pack, it's because you haven't remembered to add yourself to the vboxusers group.

To do that:

# usermod -a -G vboxusers your-user-name

...and then you'll probably want to log out and log back in so your UI processes pick up the change.

Thursday 29 May 2014

Chrome and Java

If you use Chrome and are unfortunate enough to still have to use Java applets from time-to-time, you may recently have been unpleasantly surprised to find that the Java plug-in stopped working (if you use Linux), or you soon will be (if you use Windows; I don't know when/if this is happening on Macs).

Why? Because Chrome is discontinuing support for the venerable old NPAPI, the mechanism that the Java plug-in uses to hook into the browser, and Oracle hasn't released a Java plug-in using the newer PPAPI that Google touts.

"Does the browser tell you that?" you ask
No, it just acts like the plugin isn't installed at all.

"Not even if you enable the debug log with the verbose option?"
Nope.

"Then what does it do?"
It shows you a message saying you need Java and gives you a link to Oracle's website to download it, even though doing so will do you no good whatsoever.

"Isn't that...just really amazingly stupid?"
Oh yes.

Let's be clear: I'm not saying discontinuing support for the NPAPI is a bad idea, just that doing so in such a half-baked fashion is about has hamfisted as it gets. I wasted far too much time trying to figure out why Java suddenly stopped working when a simple "Chrome no longer supports the plug-in you have installed to handle this content" with a link to this page would have told me what was going on.

Which is why I'm telling you, dear reader. :-) And you might want to join me in telling them to be more clear.


Tangent: But we shouldn't be using Java applets anyway, right? Well, there are some who would argue that, and certainly Oracle's botched handling of Java on the client (and Sun's before them) has done a lot over the years to destroy applets, and better in-browser options have reduced the need for them. But those significant codebases won't just update themselves, and there are still things you can do in a Java applet (signed applet, of course) that you just can't do otherwise. One of my clients uses a VPN that launches via a signed Java applet, for instance. I can't just tell them to get lost because Google decided NPAPI was a bad thing. Google's own figures show that they're inconveniencing 9% of their users by doing this. That ain't nuthin. So applets are niche, but still there.

Monday 17 March 2014

Session tokens, cookies, and local storage

Link-post today, the Meteor blog has a very interesting post about why Meteor has so-far avoided session cookies in favor of localStorage, including a high-level but nevertheless useful overview of issues with session cookies. Useful read for the security-minded, including an interesting suggestion near the end of a "both and" approach.

Tuesday 18 February 2014

Centering Made Easy

Almost a note-to-self today. :-) Now that IE6 and IE7 are dead unless you need to support users from the far East, centering content within elements using CSS is dead easy. Three simple rules:

display:        table-cell;
text-align:     center;
vertical-align: middle;

So for instance:

<style>
  .content {
    width: 300px;
    height: 300px;
    border: 1px solid #aaa;
    padding: 2px;
    display: table-cell;
    text-align: center;
    vertical-align: middle;
  }
</style>
<div class="content">
    Here is <strong>my content</strong> with
    <em>markup</em>; my content wraps in the container,
    but that's not a problem for this technique.
</div>

That comes out with the content nicely displayed centered both horizontally and vertically. Unlike the line-height trick, it works well with content that wraps. Here's a live example.

I'm not saying we should have to say things are table-cells when they're not, but at least it's only in the presentation layer, not the markup.