Skip to content

Refactor to async-await (#521) #973

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

Closed
wants to merge 3 commits into from

Conversation

siddhant1
Copy link
Contributor

@siddhant1 siddhant1 commented Mar 22, 2019

I have verified that this pull request:

  • has no linting errors (npm run lint)
  • [ x] is from a uniquely-named feature branch and has been rebased on top of the latest master. (If I was asked to make more changes, I have made sure to rebase onto master then too)
  • [x ] is descriptively named and links to an issue number, i.e. Fixes #521

@siddhant1
Copy link
Contributor Author

@catarak I am working on working on all the controllers , Should I make separate prs or add commits here?

@siddhant1
Copy link
Contributor Author

I am currently having some linting error because of my global prettier settings, I will fix them at the end.

@siddhant1
Copy link
Contributor Author

ref #521

Copy link
Member

@meiamsome meiamsome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, generally, here are some comments on what you have so far.

I'd suggest keeping the PRs small so they can be dealt with quicker.

res.json(updatedProject.files[updatedProject.files.length - 1]);
});
} catch (err) {
if (err) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If statement is not required. The return after the error message can also be removed.

.reduce((acc, childId) => (
[...acc, childId, ...getAllDescendantIds(files, childId)]
), []);
return parentFile.children.reduce(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as a heads up, these stylistic changes should really not be included, it breaks the git blame for this line and is generally unhelpful when looking at diffs.

That said, making an additional commit to revert them would be worse in general, so only fix that if you're going to be doing it by altering commit history. I just thought I'd point out that it is something to look out for when you're creating a PR: If you have diffs on lines irrelevant to what you're doing, something usually has gone wrong :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My global prettier generally makes these changes , since the project does not has any local .prettierrc file , my global prettier kicks in and make the changes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for this project, i squash commits for a PR, so i'm okay with you making commits to revert stuff. i also agree, these stylistic changes should not be included in this PR.

res.json(project.files);
} catch (err) {
// NOT SO GREAT ERROR HANDLING
console.log(err);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably respond with a 500 error here so that the API user always gets a response - though don't send the user the error

return;
}
res.send(resolvedFile.content);
});
} catch (err) {
if (err) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another unnecessary if here & return below

}
const newFile = updatedProject.files[updatedProject.files.length - 1];
updatedProject.files.id(req.body.parentId).children.push(newFile.id);
updatedProject.save(innerErr => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be promisified too.

});
} catch (err) {
get404Sketch(html => res.send(html));
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can delete this return

@siddhant1
Copy link
Contributor Author

Thanks for the review , I will make the changes!

@siddhant1
Copy link
Contributor Author

@meiamsome @catarak Should I make another pr when I am done refactoring other files?

@catarak
Copy link
Member

catarak commented Mar 25, 2019

@meiamsome @catarak Should I make another pr when I am done refactoring other files?
honestly, feel free to do one PR per controller. i prefer smaller PRs as they are easier to test and merge.

const filesToInject = files.filter(file => file.name.match(/\.(js|css)$/i));
injectMediaUrls(filesToInject, files, req.params.project_id);
export async function serveProject(req, res) {
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not a fan of the try body being huge here. it's only here to catch errors from mongodb, right? so only line 13 should be in the body.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it also catches all errors that are unintended to make sure that the API user gets a response (500) when the server has a problem, instead of just having the connection hang. In non-async handlers this is handled by default by express because they try/catch all the handlers, but express doesn't support async natively. There is express-async-handler which would help us wrap these functions correctly to catch those errors, but you're just moving where the wrapping handler is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, i'm confused. are you saying that express doesn't support async/await in the handler functions?

Copy link
Member

@meiamsome meiamsome Mar 26, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's right, it doesn't natively handle it, it expects the user to either throw errors synchronously, or pass them all to next() manually. Indeed if you look at the source of that async wrapper, it is essentially just doing this large try/catch there. It's a shame that express doesn't understand Promises, but the people behind express decided it'd be too dramatic a change for express to support them. (As an aside they actually decided to redesign express entirely and made Koa which is great but differs in some ways.)

So, that leaves you with two options:

  1. Don't use async/await and lose the great syntactical bonuses that gives you, keeping in mind that you still need to handle the asynchronous errors in your promise chains by eventually passing them to next():
function (req, res, next) {
  // Something synchronous here
  promise
    .then(() => doSomething())
    .catch(next);
}
  1. Do use async/await and be stuck with wrapping each route's handler in some sort of wrapper, be that a library one or just a big try/catch. The standard try/catch is something akin to:
async function (req, res, next) {
  try {
    // Really long function here
  } catch(e) {
    next(e);
  }
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow, okay. i had no idea! thanks for the detailed explanation. my instinct is to use the express-async-handler middleware, as wrapping everything in a try/catch feels like it gets redundant fast. just to make sure i totally understand this, say the middleware were included in the project, and i wanted to catch errors specific to the mongodb query/connection. that would look like

let project;
try {
  project = await Project.findById(req.params.project_id);
} catch(e) {
  // do something with the error, e.g. return 404 or 500
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ganeshpatro321 thanks for the update! that's super frustrating. i'm starting to feel like nested callbacks is better than this mess...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only problem with callbacks is not nesting or callbacks hell , I think if not now , we should definitely consider this as an important task in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@siddhant1 i agree! i'm not a huge fan of the nested callbacks. however, it feels like the tools to move everything to async/await are in flux, and i think it's best to wait a little bit.

one option could be to just move to promises. but, honestly, and maybe this is just me, but i'm not sure the promise syntax is too much cleaner/easier to read without async/await

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important to note that one of the major benefits of using Promises/(async/await) is the ability to have errors bubble up naturally, rather than not being sure whether some errors may just be being silently ignored instead of raised correctly. As discussed before, Koa is a viable Express replacement that is functionally identical except for supporting promises natively. I would be happy to do an exploration PR into swapping out for that if you think it makes sense to have a look at it @catarak ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is a great point.

it seems like koa is a fairly radical departure from express, and would involve some major changes. for me, there are higher priority issues, so working on this change would have to be fairly self-directed. however, i am happy to test and give support when i can. if you think there's a possibility of significantly improved error-handling and major code clean up, it's definitely worth investigating!

res.json({ success: false });
return;
export async function createFile(req, res) {
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue here. i don't like the giant try body.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense , I will refactor

);
const newFile = updatedProject.files[updatedProject.files.length - 1];
updatedProject.files.id(req.body.parentId).children.push(newFile.id);
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

especially nested try/catch... i would try to avoid it if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to avoid them?

Copy link

@anku255 anku255 Apr 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@catarak @siddhant1 I have a suggestion that can clear up this try catch litter. We can define a wrapper function to catch an error and wrap every async function with it. This way we can avoid try/catch with async await.

/* Helper buddy for removing async/await try/catch litter.  
 We can make the name of this function a little smaller */
function catchError(promise) {
  return promise.then(data => {
    if (data instanceof Error) return [data]
    return [null, data]
  }).catch(err => [err])
}

// Example Usage
async function usageExample(params) {
  const [ err, data ] = await catchError(myPromise(params)) // We are using the wrapper here

  if (err) {
    // handle error
  }
  // Do stuff with data
}

What do you think about it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmmm, i prefer express-async-handler as i like that it handles the error handling as middleware. to me, it feels like a cleaner solution, rather than adding a helper function to every single controller function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Express-async-handler is good
I will refactor it along with adding a better error handling approach , because for now we are just logging the errors

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i recommend not just pushing ahead on something. i think there are a few different options here and i'd like to see a comparison of these options, so we can make a decision about what the best path forward is.

@catarak
Copy link
Member

catarak commented Feb 4, 2020

closing this PR due to inactivity, but making sure this conversation is linked to in #521.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants