Reputation: 1680
I'm working on an application that uses what-wg fetch all over the place. We've defined default fetch middleware and options this way:
export function fetchMiddleware(response) {
return new Promise(resolve => {
resolve(checkStatus(response));
}).then(parseJSON);
}
export const fetchDefaults = {
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
};
We use our default middleware / fetch options this way:
fetch('/api/specific/route', fetchDefaults)
.then(fetchMiddleware)
.then(function(data) {
// ... Dispatch case-specific fetch outcome
dispatch(specificRouteResponseReceived(data));
});
We want to add a generic, fallback catch to all fetch
usages throughout the application, in other words something like this:
export function fetchGenericCatch(function(error) {
showGenericErrorFlashMessage();
})
fetch('/en/api/user/me/preferences', fetchDefaults)
.then(fetchMiddleware)
.then(function(data) {
dispatch(userPreferencesReceived(data));
})
.catch(fetchGenericCatch);
Lots of code duplication. We'd like a utility function / class which can do all of this for us, e.g. something which would work like this:
genericFetch('/api/specific/route') // bakes in fetchDefaults and fetchMiddleware and fetchGenericCatch
.then(function(data) {
dispatch(userPreferencesReceived(data));
}); // gets generic failure handler for free
genericFetch('/api/specific/route') // bakes in fetchDefaults and fetchMiddleware and fetchGenericCatch
.then(function(data) {
dispatch(userPreferencesReceived(data));
})
.catch(function(error) {
// ...
}); // short-circuits generic error handler with case-specific error handler
The main caveat is that the generic catch
must be chained after the case-specific then
s / catch
es.
Any tips on how this might be achieved using whatwg-fetch / ES6 Promises?
Related:
There are similar posts, but they don't seem to address the need for a default catch which runs after all non-default then
s and catch
es:
Edit 14 Oct:
Possible duplicate: Promises and generic .catch() statements
Upvotes: 2
Views: 2189
Reputation: 1680
I think the solution is as simple as:
export function genericFetch(url, promise, optionOverrides) {
const fetchOptions = {...fetchDefaults, ...optionOverrides};
return fetch(url, fetchOptions)
.then(fetchMiddleware)
.then(promise)
.catch(function(error) {
showGenericFlashMessage();
});
}
A use case which doesn't need a special error handler can simply use it this way:
genericFetch('/api/url', function(data) {
dispatch(apiResponseReceived(data));
});
A use case which needs a special catch, or a more complex chain, can pass in a full-blown promise:
genericFetch('/api/url', function(response) {
return new Promise(resolve, reject => {
dispatch(apiResponseReceived(data));
}).catch(nonGenericCaseSpecificCatch); // short-circuits default catch
});
Upvotes: 0
Reputation: 222493
Having WET code isn't the worst option here, as long as error handler is DRY.
fetch(...)
...
.catch(importedFetchHandler);
It causes no problems and conforms to the behaviour of Bluebird and V8 promises, where unhandled rejection event exists to make sure that no promise are left uncaught.
The most simple way to reach this is to introduce promise-like wrapper for fetch
promise:
function CatchyPromiseLike(originalPromise) {
this._promise = originalPromise;
this._catchyPromise = Promise.resolve()
.then(() => this._promise)
.catch((err) => {
console.error('caught', err);
});
// every method but 'constructor' from Promise.prototype
const methods = ['then', 'catch'];
for (const method of methods) {
this[method] = function (...args) {
this._promise = this._promise[method](...args);
return this;
}
}
}
which can be used like
function catchyFetch(...args) {
return new CatchyPromiseLike(fetch(...args));
}
A promise-like like that has natural limitations.
Side effects are discarded if converted to real promise:
Promise.resolve(catchyFetch(...)).then(() => /* won't be caught */);
And it won't play well with asynchronous chain (this is a no-no for all promises):
var promise = catchyFetch(...);
setTimeout(() => {
promise.then(() => /* won't be caught */);
});
A good alternative is Bluebird, no need to invent the wheel there, features is what it is loved for. Local rejection events look like exactly what's needed:
// An important part here,
// only the promises used by catchyFetch should be affected
const CatchyPromise = Bluebird.getNewLibraryCopy();
CatchyPromise.onPossiblyUnhandledRejection((err) => {
console.error('caught', err);
});
function catchyFetch(...args) {
return CatchyPromise.resolve(fetch(...args));
}
Phew, that was easy.
Upvotes: 2