-
Notifications
You must be signed in to change notification settings - Fork 17
fix Promise error handling #3022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3022 +/- ##
==========================================
+ Coverage 98.25% 98.25% +<.01%
==========================================
Files 253 253
Lines 12029 12050 +21
==========================================
+ Hits 11819 11840 +21
Misses 210 210
Continue to review full report at Codecov.
|
If you have a promise chain like this:
We would be handling errors from either |
Yes, exactly. And, in particular, in our action dispatchers, which look like this: export function fetchProgramEnrollments(): Dispatcher<AvailablePrograms> {
return (dispatch: Dispatch) => {
dispatch(requestGetProgramEnrollments());
return api.getPrograms().
then(enrollments => dispatch(receiveGetProgramEnrollmentsSuccess(enrollments))).
catch(error => {
dispatch(receiveGetProgramEnrollmentsFailure(error));
// the exception is assumed handled and will not be propagated
});
};
} the This comment probably explains the problem better than I will be able to. |
In api.js:125 there's a |
@@ -50,7 +48,7 @@ export const receiveAddProgramEnrollmentSuccess = createAction(RECEIVE_ADD_PROGR | |||
export const RECEIVE_ADD_PROGRAM_ENROLLMENT_FAILURE = 'RECEIVE_ADD_PROGRAM_ENROLLMENT_FAILURE'; | |||
export const receiveAddProgramEnrollmentFailure = createAction(RECEIVE_ADD_PROGRAM_ENROLLMENT_FAILURE); | |||
|
|||
export const addProgramEnrollment = (programId: number): Dispatcher<AvailableProgram> => { | |||
export const addProgramEnrollment = (programId: number): Dispatcher<?AvailableProgram> => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to mark this with ?
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because the onResolved
handler doesn't actually return a Promise<AvailableProgram>
, it just returns undefined
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
before I think Flow wasn't able to accurately type-check things.
d19f2b2
to
17558e9
Compare
17558e9
to
49c517e
Compare
ok! I re-wrote |
49c517e
to
c97d942
Compare
c97d942
to
d4b911e
Compare
static/js/lib/api.js
Outdated
@@ -92,12 +93,17 @@ import { fetchWithCSRF } from './api'; | |||
* - non 2xx status codes will reject the promise returned |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this comment down to _fetchJSONWithCSRF
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And also add a comment explaining about the conversion from maybe to promise in case someone isn't familiar with sanctuary
d4b911e
to
f4d5dbe
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a comment about a comment, looks good 👍
static/js/lib/api.js
Outdated
@@ -84,6 +85,15 @@ const _fetchWithCSRF = (path: string, init: Object = {}): Promise<*> => { | |||
export { _fetchWithCSRF as fetchWithCSRF }; | |||
import { fetchWithCSRF } from './api'; | |||
|
|||
const resolveEither = S.either( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment here in case someone is unfamiliar with sanctuary?
In many places where we use Promises (e.g. for making fetch requests to our backend APIs) we were previously incorrectly handling promise rejection. We were often doing `promise.then(fn).catch(fn)`, which is not a great idea - then the `.catch` handler catches any errors in the Promise AND any errors in the `.then` handler. Instead, we should prefer to do `promise.then(resolve, reject)` - this makes the `reject` handler narrower in it's responsibility. The main problem with the former pattern is that the `.catch` handler can swallow any exceptions which occur in the `.then` handler, and we will not see them. This can lead to strange and unhelpful stack traces (such as the react `getHostNode` error that crops up sometimes). Anyway, this PR should fix it! I also simplified and streamlined our `fetchJSONWithCSRF` function while I was at it. pr #3022
f4d5dbe
to
824fd75
Compare
What are the relevant tickets?
none
What's this PR do?
I was looking at a JavaScript error in Sentry this morning and I realized (after reading this issue, among others) that our error handling for
Promises
is done incorrectly, in a way that can swallow errors and lead to some strange behavior.In short, these two things are not equivalent:
basically, the first problem is an issue because
.catch
becomes overly broad. Instead of just handling therejected
case forpromise
, it also catches any errors that happen in the.then
handler. This can, in certain cases, include rendering / UI errors which are not related to thepromise
itself, but related to an error in the code we use to handle or render the data once it comes back. In this case, the UI error will be silently swallowed by the.catch
(because it's occurring in the.then
handler) and we won't see it. Then, often, the error will show up somewhat cryptically with a different stack trace.this is a problem in particular with redux, because our async (redux-thunk) action dispatchers trigger a
render()
call whendispatch
fires, so that if there is an error anywhere in thatrender()
call (say, because of wacky data) it will trace back to thedispatch()
and be silently swallowed by.catch
. yucky.The correct way to use promises is
.then(onResolve, onReject)
. Then our handler functions are strictly confined to dealing with resolution or rejection of the original promise itself. Of course, if want to chain off of this function that is fine and dandy as well, but we need to make sure that there is anonReject
handler for the initial promise beforehand, otherwise the error can be propagated somewhat strangely.Anyway, I believe this is the root cause of a React error we're seeing rarely but consistently (
Cannot read property 'getHostNode' of null
).two other React issues relating to this:
facebook/react#8267
facebook/react#4199
How should this be manually tested?
tests should pass, app should behave normally, there should be no regressions.