2

I have this piece of code that calls a function getTableData and expects a Promise in return.

function populateTableRows(url) {
  successCallback = () => { ... };
  errorCallback = () => { ... };

  getTableData(url, successCallback, errorCallback).then(tableData => {
    // do stuff with tableData
  }
}

This is used in many places across my codebase, and I'm looking to keep the behavior the same as I move away from using jQuery's ajax (and jQuery in general)

In getTableData, I'm currently using $.ajax like so

function getTableData(url, successCallback, errorCallback) {
  successCallback = successCallback || function() {};
  errorCallback = errorCallback || function() {};

  const ajaxOptions = {
    type: 'POST',
    url: url,
    dataType: 'json',
    xhrFields: {
      withCredentials: true
    },
    crossDomain: true,
    data: { // some data }
  };

  return $.ajax(ajaxOptions).done(successCallback).fail(errorCallback);
}

This currently returns a Promise for successful requests. For bad requests where fail is invoked, it doesn't appear that a Promise is returned and the then doesn't run in the calling function (which is okay in this case).

When converting the request over to use fetch, I have something like this

function getTableData(url, successCallback, errorCallback) {
  successCallback = successCallback || function() {};
  errorCallback = errorCallback || function() {};
  return fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    credentials: 'include',
    body: { // some data }
  })
  .then(response => {
    let json = response.json();
    if (response.status >= 200 && response.status < 300) {
      successCallback(json);
      return json;
    } else {
      return json.then(error => {throw error;});
    }
  }).catch((error) => {
    errorCallback(error);
    return
  });

Successful requests appear to be behaving similarly to the ajax code that I currently have, but now the then callback is running for bad requests which is causing errors in my code.

Is there a way with fetch to mimic the fail behavior of jQuery where the Promise is seemingly aborted for bad requests? I'm fairly new to using Promises and after some experimentation/searching I haven't been able to come up with a solution.

  • 1
    Look for [response.ok](https://developer.mozilla.org/en-US/docs/Web/API/Response/ok) – Louys Patrice Bessette Jun 12 '21 at 23:50
  • 1
    Yeah the fetch MDN specifically addresses jquery.ajax differences: The fetch specification differs from jQuery.ajax() in the following significant ways: The Promise returned from fetch() won’t reject on HTTP error status even if the response is an HTTP 404 or 500. Instead, as soon as the server responds with headers, the Promise will resolve normally (with the ok property of the response set to false if the response isn’t in the range 200–299), and it will only reject on network failure or if anything prevented the request from completing. – Yuji 'Tomita' Tomita Jun 12 '21 at 23:53

2 Answers2

2

When you .catch() in a chain of promises, it means you already handled the error, and subsequent .then() calls continue successfully.

For example:

apiCall()
  .catch((error) => {
    console.log(error);
    return true; // error handled, returning true here means the promise chain can continue
  })
  .then(() => {
    console.log('still executing if the API call fails');
  });

What you want, in your case, is when you handle the error with the callback, to continue to throw it so the promise chain is broken. The chain then further needs a new .catch() block to handle the new error.

apiCall()
  .catch((error) => {
    console.log(error); // "handled", but we're still not done
    throw error; // instead of returning true, we throw the error further
    //  this can also be written as `return Promise.reject(error);`
  })
  .then(() => {
    console.log('not executing anymore if the API call fails');
  })
  .catch((error) => {
    // handle the same error we have thrown from the previous catch block
    return true; // not throwing anymore, so error is handled
  })
  .then(() => {
    console.log('always executing, since we returned true in the last catch block');
  });

By the way, what you return from one then/catch block, the following one will get it as a param.

apiCall()
  .then((response) => {
    /* do something with response */;
    return 1;
  })
  .catch((error) => { return 'a'; })
  .then((x) => console.log(x)) // x is 'a' if there's an error in the API call, or `1` otherwise
Andrei Duca
  • 372
  • 2
  • 11
  • Thanks for your input! This was informative and helped me better understand how the Promise chain operates, but it wasn't quite what I was looking for. – Will Collins Jun 14 '21 at 02:14
1

In your .catch you implicitly return undefined and thus "handle" the error. The result is a new Promise that fulfills to undefined.

  .catch((error) => {
    errorCallback(error);
    return Promise.reject();
  });

should be enough to keep the returned Promise rejecting.

Or you assign the intermediate Promise to a var and return that, and not the result to the fail handling:

  var reqPromise = fetch(url, {
    // ...
  })
  .then(response => {
    // ...
      return json.then(error => {throw error;});
  });
  
  reqPromise.catch((error) => {
    errorCallback(error);
    return
  });
  
  return reqPromise;
Tobias K.
  • 2,997
  • 2
  • 12
  • 29
  • Thanks! I tried something similar to what you mentioned with returning the rejected Promise. I was getting "Uncaught (in promise)" errors in the console, which I'm also seeing in the two solutions provided. It doesn't seem problematic, but I was curious if there is a way to handle those? – Will Collins Jun 13 '21 at 04:48
  • 1
    The Promise (Deferred) implementation of jQuery differs from the native ES6 Promise (https://stackoverflow.com/a/32832019/7362396). That's why you didn't see it there. You need to either .catch it or register a https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event unhandledrejection handler to suppress. – Tobias K. Jun 13 '21 at 09:05