Is there a way to end a thenable chain? - javascript

let textProcess = new Promise((resolve, reject) => {
let text
try {
// fetch text from the internet
text = "str"
resolve(text)
} catch (e) {
reject("failed to fetch!")
}
})
textProcess.then(data => {
let json
try {
json = JSON.parse(data)
} catch (e) {
console.error("failed to parse!")
// ..........i want to end the whole process here, never go ahead
}
}, e => {
// try to fetch text from local chache
}).then(json => {
// work on the json obj
}, e => {
// if failed to fetch from local too, just let it go
})
Is there a way to end a thenable chain?
Look at the example above, I want to end the whole process when parsing is failed(the line preceeded with ".........."). But actually the last then will still be invoked though.
What is the proper and elegant way to achieve my goal?

Your Promise usage involves quite a bit of sub-optimal patterns. Fixing them actually leads to what you're trying to achieve too.
textProcess.then(data => {
// 1st anti-pattern fix
// any error triggered here
// also gets caught at the end catch
return JSON.parse(data)
}).then(json => {
// work on json obj
}).catch(e => {
// 2nd anti-pattern fix
// one catch for the whole thenable chain
console.error("Failed to parse!", e)
})
This way, you properly leverage what Javascript Promise offers, and one simple .catch for what you need.
Edit - some explanations on involved Promise anti-patterns
The marked 1st anti-pattern is about unnecessary nested try..catch block within then. Within it, you can return synchronously (even undefined), another Promise (both of these are thenable), or throw an Error (which would get caught by catch). Basically you don't need to explicitly catch it but let it "flow" through.
The 2nd anti-pattern as mentioned is the fact that the second parameter -- reject handler of then is considered sub-optimal in most use cases. A Promise chain should be leveraging one catch to simplify the workflow.
However, in the rare event of the need to perform "early catch" and "resume", consider the following way, which is still a bit clearer than using two handlers for then:
textProcess.then(data => {
return parser1(data)
}).catch(e => {
console.log("First parser failed")
// for example first parser failed
return "fallback data"
}).then(data => {
// process received data as "fallback data"
}).catch(e => {
// always have a "last resort" catch at the end of the workflow
})

The short answer is no, there is no mechanism to terminate a promise chain in a then handler part-way down the chain. (A proposal for promise cancellation was made to the TC39 committee in 2016 but was subsequently withdrawn.)
Note that a "promise chain" often refers to the promise
returned by the last then, catch or finally call in a chain of single promise method calls concatenated together.
All calls to the promise methods listed above are made synchronously when code defining the chain is executed. After execution, all promises in the chain have been created and all method calls in the chain called.
Since promises only have three states (pending, fulfilled and rejected), the best you can do is to arrange for "cancellation" to be sent down the rejection channel of linked promises and ignore it as required. (There is no standard "cancelled" rejection value to use).

Related

Can I skip a catch block under certain conditions in a try/catch?

I have a function which gets given a list of ids and then maps over that list and calls an API for each one:
let fullDetails: Array<any> = [];
ids.map(async (id) => {
try {
const result = await api.getFullDetails(id);
if (result.data) fullDetails.push(result.data);
} catch {
// Handle error
}
});
The problem I'm having is that sometimes the getFullDetails function will return an error just because the record it's looking for doesn't exist. But, I don't really care if the record doesn't exist (to be honest, I don't really care about any errors here) - I'm happy to just skip that one and move on to the next. No matter what I do, though, my code seems to bail out at that point if the result is an error.
I've tried leaving out the try-catch block, but then I get a 'Possible unhandled Promise rejection' error and fullDetails remains empty (although I know for sure that one of the ids worked ok).
I also tried rewriting to use Promise.all, like this:
let results = ids.map((id) =>
api.getFullDetails(id),
);
Promise.all(results)
.then((result) => {
console.log(result);
})
.catch((error) => { console.log('Error')});
but again, it goes into the catch block if there's any kind of error. Again I tried leaving out the catch block here, but then I got the 'Possible unhandled Promise rejection' error again, and the result was never shown.
Is there a way to handle this (apart form rewriting the API to not return an error)? Basically I just don't want to check for errors at all, and just ignore them if they occur.
The proper way to use map with async is to use one of the Promise's methods.
It's better to choose Promise.allSettled() rather than Promise.all() in your case. Because, according to MDN,
In comparison, the Promise returned by Promise.all() may be more appropriate if the tasks are dependent on each other, or if you'd like to immediately reject upon any of them rejecting.
Meaning the former case won't reject promises and stop the program execution.
Note that the allSettled() returns an object with two of this three properties:
[{
reason: "Id must be non-negative",
status: "rejected"
}, {
status: "fulfilled",
value: 2
}]
The first case happens when the Promise is rejected and the second when it's fullfiled.
Promise.allSettled(ids.map((id) => api.getFullDetails(id))).then(x => {
let fullDetails = x.map(({value})=>value).filter(Boolean)
console.log(fullDetails)
})
As seen in this working example.

How to use a single default catch in a double returned promise?

Basically I have a construction like;
// Library function
function foo(){
return new Promise((resolve, reject) =>{
// Do lots of stuff, like rendering a prompt
// On user action
if(userDidSomething){
resolve(user_input);
}
if(userCanceled){
// On user cancel
reject('User canceled');
}
}).catch("Default error (usually canceling whatever it is, which means; do nothing)");
}
// Function to call library function with predefined settings for this website
function bar(){
// Have some defaults set here, like which kind of prompt it should be
return foo();
}
// Where the function will actually be used
function baz(){
bar().then("do some things");
}
I've worked my way around this issue some years ago but unfortunately forgot how I actually managed to do that.
The goal: Have one standard catch handle things for me on the library level. If I want to overrule it, I can always do that later. So I guess it's: Break the chain.
The problem: Having the catch before the then, causes the then to be triggered once I have dealt with the catch (which is; ignoring it, in this case)
My current solution: I'm using throw on the library-level catch, this causes the promise to throw an uncaught error exception.
However, this completely clutters up my console with errors which aren't really errors.
The problem is that the entire resolve/reject of the promise is being handled by the library. That promise gets returned around and I only call it way later.
This is a function I'm calling about 300 times throughout my project, and I don't want to be putting custom error handling on every single one of those function calls when the handling of this should be "don't do anything, really".
What you are asking for is not really possible using native Promises because that's not how they were intended to be used.
You can, though, create a custom Promise class that does this. Note that muting all rejections is a bad idea because you won't see if your code errors out. To go around that, only a special value (SpecialPromise.CANCELED) is treated differently. The code has to track whether a .catch is attached to it. When the promise encounters the special value and it has no catch callback at the moment, it quickly attaches a no-op catch callback to silence the error:
class SilentPromise extends Promise{
constructor(executor){
super((resolve, reject) => {
executor(
resolve,
e => {
if(e === this.constructor.CANCELED && !this._hasCatch){
this.catch(() => {})
}
reject(e)
}
)
})
this._hasCatch = false
}
then(success, error){
this._hasCatch = true
return super.then(success, error)
}
catch(error){
this._hasCatch = true
return super.catch(error)
}
catchIfNotCanceled(error){
this.catch(e => {
if(e === this.constructor.CANCELED)
throw e
return error(e)
})
}
}
SilentPromise[Symbol.species] = SilentPromise
SilentPromise.CANCELED = Symbol('CANCELED')
You can convert existing promises between SilentPromise and Promise using SilentPromise.resolve() and Promise.resolve().

Recursive JavaScript Promises (auto paging an API)

I have a paged API that I need to automatically fetch each page of results.
I have constructed the following recursive promise chain, which actually gives me the desired output. I have renamed the traditional resolve to outer to try and wrap my head around it a little more but I am still confused at how passing the outer function into the nested .then is the magic sauce that is allowing this to work?
I thought I knew Promises well but obviously not!
protected execute<T>(httpOptions): Promise<AxiosResponse | T> {
const reqObject: HttpConfig = {
...httpOptions,
baseURL: this.config.baseUri,
headers: { Authorization: `Bearer ${this.config.apiKey}` },
};
return new Promise((outer, reject) => {
axios
.request<T>(reqObject)
.then((res) => {
if (res.headers.link) {
const pagingObject: PagingObject = this.getPagingObject(res);
this.pagedListResponses = this.pagedListResponses.concat(res.data);
if (pagingObject.last) {
this.execute<T>({
url: pagingObject.next,
}).then(outer);
} else {
outer(this.pagedListResponses as any);
}
} else {
outer("No paging header found");
}
})
.catch((err) => reject(err));
});
}
TL:DR
As you add more and more Promises via recursion they pile up like the skin of an onion. When each is resolved and .then() is called, the Promises are removed from the inside out until there are none left.
Promise Chaining
When a Promise object is created within the Resolve handler
of another Promise both Promises are settled in an inside-out order. Additionally, when chaining is used through
the Promise instance methods (.then(), .catch(), .always(), etc), the chaining methods take priority and are
executed before the outermost Promise object resolves.
Perhaps better explained here.
Your code creates the Axios Promise within the Resolve handler of the outer construced Promise Object. The
AxiosPromise's .then() will execute prior to the outer Promise finally settling. After that happens the result is passed through the outer Promise object with no modification or processing. Basically a no-op.
That is why the wrapper Promise (explicit-construction anti-pattern)
is unecessary and discouraged - it is a waste of time and resources that provides no benefit in this code.
With recursion in the mix, Promise objects just keep getting piled on.
So (for now) a reference to that outer Promise object is returned from the .execute() method. But when/how is it settled (resolved or rejected)?
The Axios Promise resolves with AJAX results
If this is a recursive call, this is an inner Promise (even though created with .then(outer) which is more confusing in this case)
The outer most .then() is called with the AJAX results (or rejected with reason)
The instance variable pagedListResponses is updated with the results
if res.headers.link == true && pageObject.last == true
Recurse & start back at 1 <- pending with unsettled Promise(s)
else if res.headers.link == true && pageObject.last == false
resolve the outer most Promise with the pageListResponses <- completely settled
else if res.headers.link == false && pageObject.last == false
resolve the outer most Promise with "No paging header found" <- completely settled
Then then() method attached to the initial call to execute() is called with the pageListResponses
ex. this.execute({...}).then(pageList=>doSomethingWithPageOfResults());
So chaining, using .then() midstream, allows us to do some data processing (as you have done) prior to returning an
eventually settled Promise result.
In the recursive code:
this.execute<T>({
url: pagingObject.next,
}).then(outer);
The .then() call here simply adds a new inner Promise to the chain and, as you realize, is exactly the same as writing:
.then(result=>outer(result)); Reference
Async/Await
Finally, using Async / Await is recommended. It is strongly suggested to Rewrite Promise code with Async/Await for complex (or some say any) scenarios.
Although still asychronous, this makes reading and rationalizing about the code sequence much easier.
Your code rewritten to take advantage of Async/Await:
protected async execute<T>(httpOptions): Promise<T> {
const reqObject = {
...httpOptions,
baseURL: this.config.baseUri,
headers: {Authorization: `Bearer ${this.config.apiKey}`},
};
try {
const res: AxiosResponse<T> = await axios.request<T>(reqObject);
if (res) {
const pagingObject: boolean = this.getPagingObject(res);
this.pagedListResponses = this.pagedListResponses.concat(res.data);
if (pagingObject) {
return this.execute<T>({ url: 'https://jsonplaceholder.typicode.com/users/1/todos'});
} else {
return this.pagedListResponses as any;
}
} else {
return "No paging header found" as any;
}
} catch (err) {
throw new Error('Unable to complete request for data.');
}
}
Here, async directly indicates "This method returns a Promise" (as typed). The await keyword is used in code only once to wait for the resolution
of the AJAX request. That statement returns the actual result (as typed) as opposed to a Promise object. What happens with a network error? Note the use of try/catch here. The catch block handles any reject in the nest/chain. You might notice the recursive call has no await keyword. It isn't necessary in this case since it is a Promise that just passes along its result. (it's OK to add await here but it has no real benefit).
This code is used the same way from the outside:
this.execute({...}).then(pageList=>doSomethingWithPageOfResults());
Let me know what gaps you may still have.
Well when you call this.execute, it returns a Promise<AxiosResponse | T>.
So by calling .then(outer), the response from the recursive execute function is being used to resolve your outer promise.
It's basically the same as this.execute(...).then(response => outer(response)), if that makes it any clearer.

Promise: skip all fulfill and reject reactions but execute .finally

I have function called request:
function request (endpoint) {
return axios.request(endpoint).then(api.onSuccess).catch(api.onError)
}
api.onSuccess:
onSuccess (response) {
let breakChain = false
... some logic goes here ...
return breakChain ? (new Promise(() => {})) : response
}
api.onError:
onError (error) {
let breakChain = false
... some logic goes here ...
if (breakChain) {
return new Promise(() => {})
} else {
throw error
}
}
api holds a lot of functions that represent different API calls based on provided endpoints data and return request(endpoint).
Currenly I have code, as you can see, that return Promise with empty executor that is always in pending state to stop the chain of subsequent .then(...) and .catch(...) handlers from execution as they just infinitely wait for that Promise to settle. This is done to handle certain API responses that have common response handling (like responses with code >= 500).
The problem is that now I need a call to .finally() (like in Vue cookbook - https://v2.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html#Dealing-with-Errors) to restore some component's state nevertheless of whether there is an error or not, but this approach of pending Promise is an obstacle.
The question is: is it possible to skip all subsequent .then(...) and .catch(...) calls within one of such handlers to go directly to .finally()?
Update. I haven't mentioned the important bit - api.onSuccess and api.onError are basic handlers. In another application components there are additional handlers appended to the end of that basic chain presented in request function. Usual chain of some API call has a following resulting form:
return axios.request(endpoint).then(api.onSuccess).catch(api.onError).then((response) => {...}).catch(() => {...}).finally(() => {...})
(sometimes there is no .finally() or .catch(...) block)
Is it possible to skip all subsequent .then(...) and .catch(...) calls within one of such handlers to go directly to .finally()?
No.
Currenly I stop the chain by just infinitely waiting - yet this approach of pending Promise is an obstacle.
Indeed, don't do that. You can skip then handlers by using rejections (exceptions) for flow control, but the more appropriate way is to handle this by nesting the part of the chain to be skipped inside an if statement.
This is done to handle certain API responses that have common response handling (like responses with code >= 500)
For that, you should use something like
return axios.request(endpoint).then(response => {
…
}).catch(error => {
if (api.handleCommonError(error)) return; // returns false if it couldn't handle the error
…
}).finally(() => {
…
});
Yes, you cannot hide this kind of error handling inside an api.request function.
You can use async and await. All modern browsers support them, and your bundler can make them compatible with older browsers.
For example:
async function request (endpoint) {
try {
const response = await axios.request(endpoint);
return api.onSuccess(response);
} catch (err) {
api.onError(err);
} finally {
// Always executed, even if no error was thrown
}
}
You can also do it more traditionally:
function request (endpoint) {
return axios.request(endpoint).then(api.onSuccess, api.onError).then(() => {
// Code is always executed after error / success
}
}

How to handle async errors correctly?

When making a GraphQL query, and the query fails, Apollo solves this by having a data-object and an error-object.
When an async error is happening, we get the same functionality with one data-object and one error-object. But, this time we get an UnhandledPromiseRejectionWarning too, with information about: DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code..
So, we obviously need to solve this, but we want our async-functions to cast errors all the way up to Apollo. Do we need to try...catch all functions and just pass our error further up the tree? Coming from C#, were an exception just goes all the way to the top if never caught, it sounds like a tedious job to tell Apollo GraphQL that one (or more) leaves failed to retrieve data from the database.
Is there a better way to solve this, or is there any way to tell javascript/node that an uncaught error should be passed further up the call tree, until it's caught?
If you correctly chain your promises, you should never see this warning and all of your errors will be caught by GraphQL. Assume we have these two functions that return a Promise, the latter of which always rejects:
async function doSomething() {
return
}
async function alwaysReject() {
return Promise.reject(new Error('Oh no!'))
}
First, some correct examples:
someField: async () => {
await alwaysReject()
await doSomething()
},
// Or without async/await syntax
someField: () => {
return alwaysReject()
.then(() => {
return doSomething()
})
// or...
return alwaysReject().then(doSomething)
},
In all of these cases, you'll see the error inside the errors array and no warning in your console. We could reverse the order of the functions (calling doSomething first) and this would still be the case.
Now, let's break our code:
someField: async () => {
alwaysReject()
await doSomething()
},
someField: () => {
alwaysReject() // <-- Note the missing return
.then(() => {
return doSomething()
})
},
In these examples, we're firing off the function, but we're not awaiting the returned Promise. That means execution of our resolver continues. If the unawaited Promise resolves, there's nothing we can do with its result -- if it rejects, there's nothing we can do about the error (it's unhandled, as the warning indicates).
In general, you should always ensure your Promises are chained correctly as shown above. This is significantly easier to do with async/await syntax, since it's exceptionally easy to miss a return without it.
What about side effects?
There may be functions that return a Promise that you want to run, but don't want to pause your resolver's execution for. Whether the Promise resolves or returns is irrelevant to what your resolver returns, you just need it to run. In these cases, we just need a catch to handle the promise being rejected:
someField: async () => {
alwaysReject()
.catch((error) => {
// Do something with the error
})
await doSomething()
},
Here, we call alwaysReject and execution continues onto doSomething. If alwaysReject eventually rejects, the error will be caught and no warning will be shown in the console.
Note: These "side effects" are not awaited, meaning GraphQL execution will continue and could very well finish while they are still running. There's no way to include errors from side effects inside your GraphQL response (i.e. the errors array), at best you can just log them. If you want a particular Promise's rejection reason to show up in the response, you need to await it inside your resolver instead of treating it like a side effect.
A final word on try/catch and catch
When dealing with Promises, we often see errors caught after our function call, for example:
try {
await doSomething()
} catch (error) {
// handle error
}
return doSomething.catch((error) => {
//handle error
})
This is important inside a synchronous context (for example, when building a REST api with express). Failing to catch rejected promises will result in the familiar UnhandledPromiseRejectionWarning. However, because GraphQL's execution layer effectively functions as one giant try/catch, it's not really necessary to catch your errors as long as your Promises are chained/awaited properly. This is true unless A) you're dealing with side effects as already illustrated, or B) you want to prevent the error from bubbling up:
try {
// execution halts because we await
await alwaysReject()
catch (error) {
// error is caught, so execution will continue (unless I throw the error)
// because the resolver itself doesn't reject, the error won't be bubbled up
}
await doSomething()

Categories