4

I want to create a function that performs async operation and returns a promise. The action is:

  • downloading external content via AJAX
  • if the content can't be downloaded (e.g. invalid URL) -> reject
  • if the content was downloaded successfully, but I can't parse it -> reject
  • otherwise (content was downloaded and parsed successfully) -> resolve

The code I've got now is pretty sttraightforward:

function fetch(hash){
  return $.ajax({
    method: "GET",
    url: baseURL + "/gists/" + hash
  }).then(function(response) {
    return JSON.parse(response.files['schema.json'].content);
  });
}

it's just a jQuery AJAX call that fetches data from github gist. Currently:

  • when the AJAX can't be resolved -> promise gets rejected - OK
  • when the AJAX was resolved and content was parsed correctly -> promise gets resolved - OK
  • however, if the AJAX was resolved, but the content was invalid, the JSON.parse line throws an error and the promise doesn't get rejected and it should - FAIL

The question is - what is the right design to do that? I could wrap entire thing with a Deferred and handle it manually, but I think there might be an easier solution. Should I do a try-catch? Or something else?

ducin
  • 25,621
  • 41
  • 157
  • 256
  • This issue is caused because jQuery 1.x or 2.x promises are not compliant with the promises spec and are not throw-safe. With a real promise that follows the spec, a throw inside a `.then()` handler will automatically reject the promise. You can either cast the jQuery promise to a real promise and get the desired behavior or you will have to catch the exception yourself and return a rejected promise. – jfriend00 Jul 31 '16 at 16:18
  • FYI, I would suggest not using `fetch()` for your function name because newer browsers already have an interface named `fetch()` described [here](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). – jfriend00 Jul 31 '16 at 17:52

3 Answers3

2

I could wrap entire thing with a Deferred and handle it manually

That sounds like the deferred antipattern. Don't wrap anything that already uses promises.

I think there might be an easier solution. Should I do a try-catch?

Yes:

.then(function(response) {
  try {
    return JSON.parse(response.files['schema.json'].content);
  } catch (e) {
    return $.Deferred().reject(e);
  }
})

See also Throwing an Error in jQuery's Deferred object.

Or something else?

Actually your original code does work in any reasonable promise implementation, where exceptions become rejections implicitly. It's just jQuery's fail. You might simply want to dodge jQuery's then implementation.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Perfect answer, thx so much! Just one question about the last paragraph - does that mean that with a different promise implementation, a promise would get rejected *automatically* if an exception was thrown - and only jQuery doesn't support that? I thought that promises should automatically do that. – ducin Jul 31 '16 at 14:19
  • 1
    @ducin: Yes, exactly that. – Bergi Jul 31 '16 at 14:28
1

As has already been described, jQuery 1.x and 2.x promises are not compliant with the promise spec and .then() handlers in those version are not "throw-safe". With a proper promise, the exception will be automatically turned into a rejection.

Bergi has already shown how to manually catch the exception and manually turn it into a rejection in jQuery. But, you could also cast the jQuery promise into a real promise and then get that throw-safe behavior for free. Here's one way to do that:

function fetch(hash){
  return Promise.resolve($.ajax({
    method: "GET",
    url: baseURL + "/gists/" + hash
  })).then(function(response) {
    return JSON.parse(response.files['schema.json'].content);
  });
}

Or, perhaps even better, make a new version of $.ajax() that returns a standard promise and then use that:

$.ajaxStd = function() {
    return Promise.resolve($.ajax.apply($, arguments));
}

function fetch(hash){
  return $.ajaxStd({
    method: "GET",
    url: baseURL + "/gists/" + hash
  }).then(function(response) {
    return JSON.parse(response.files['schema.json'].content);
  });
}

Both of these options mean that your fetch() function now returns a standard promise, not a jQuery promise so it won't have some of the jQuery specific things on it like .abort(), but it will have standards-compliant promise behavior.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • in both cases you are wrapping the jQuery promise with native ES6 Promise object - or is it just _any_ promise implementation, such as 3rd party Q or buebird? ES6 is stil not the de facto standard. – ducin Aug 01 '16 at 06:05
  • @ducin - Any promise library that follows the proper specification will do including third party implementations like Bluebird and Q. ES6 promises are the standard. But, yes they aren't in all browsers in use yet (though they are in all current versions of browsers) so you can use a polyfill. – jfriend00 Aug 01 '16 at 06:13
0

As you clearly mentioned, $.ajax fails only for one of your other cases, hence you can easily handle that case alone using some custom validation. You could possibly include the parse in a try catch block

Shintu Joseph
  • 962
  • 7
  • 19