DEV Community

loading...

Discussion on: Why I Don't Use Async Await

jesterxl profile image
Jesse Warden Author • Edited

In FP style, using Promise.reject is not ok. It's just an example to show how to use. However, if you're ok with exceptions, then it is.

We're using Promise to get things for free because time and time again I see developers using async/await with NO try/catch. Not just in one function, but all the functions that use it, etc. Like, no exception handling at all. I don't believe that is a good practice to ignore exceptions. It's too hard to define which ones are important and which ones are not. JavaScript the language will help you if you use Promise and do it for you whereas async await will not. We're not pretending they're aren't any; we're acknowledging JavaScript is unpredictable so we use the best mechanism it gives us.

If you know how to use reduce, it's good. Python and JavaScript have native reduce. If you do not like it, or find it confusing, JavaScript has good support for for loops, and Python has amazing list and slicing syntax. Reduce was added after loops, so the ECMA committee found it would help a lot of people who wanted to use it, but recognized others wouldn't. That's fine, they're both there. If you like imperative style loops, they're there for you. You like list comprehensions? You can use those instead.

Maybe is a type. JavaScript doesn't really have compile time types, so Maybe is hard. Things like Folktale are great, and can sometimes help in readability, but I recognize many do not like Lodash get, or Folktale Maybe and would prefer the native optional chaining. If you understand what a Maybe is, and have a good library, then it's good. If, however, like you mentioned, you're working with people who don't, then optional chaining is good. Optional chaining was added quite recently (in ECMA years) so we had no choice but to use Maybe, else we had to do obnoxious null/undefined checks everywhere which was quite verbose. Lodash isNil/get/getOr helped a lot here.

Currying: you either like it or you don't. While the pipeline operator is years away from being usable, partial applications still work great inside of Promises. The downside is without types, it's a bit challenging to know "what comes out". As long as your build is fast, you can figure this out by re-running, but the exceptions aren't as good as Elm/Ocaml which will tell you the function type signature. If you're not familiar with currying, or are exposing your API to those that aren't, you should just expose a regular function.

Async/await syntax is great for readability

Yeah, if you like imperative code. I don't. Many do. The [error, data] example is what the Go devs do. They love it. I abhor it. Promises make it so you don't have to write Golang style.

If someone wants to use classes and a DI framework because they come from Spring Boot, and that makes them productive, they should use it. JavaScript has significantly improved class and has some good options now. Frameworks like Nest enable them to apply that Angular/Java OOP style on the server now. That is a good thing. I'll never use it myself as I don't like that style, but I will espouse to those who like OOP.

The same applies to imperative. If someone wants to use async/await, for loops, and throw exceptions, that's fine; they should continue doing so. That's not all JavaScript has to offer, though, and like Python, they support both OOP and FP styles as well. If someone doesn't like them, that's cool, but they are not Go; there are many ways to use those languages.

I just like FP style, and I like how Promise has built-in Exception handling that makes JavaScript safer for the exception cases I didn't cover.

Thread Thread
macsikora profile image
Pragmatic Maciej • Edited

I think when you say "imperative" you mean "statement based", and when you say "functional" you mean "expression based". Promises are imperative how you will use them do not change it, async/await makes promises composition more like steps, in the same way "do" syntax in Haskell. In general I think you are referring to the fact that you prefer composition of expressions instead of using assignment statement. Where for me it is no specially different, it is only lets say code style, I can say for example that let expression from Elm/Haskell in JS can be replicated by just assigning local variables, this solutions are equal until we start mutating staff.

Thread Thread
jesterxl profile image
Jesse Warden Author

Sort of. I make a ton of assumptions on the reader which I maybe shouldn't? If I don't, I end up writing 10 pages.

First, I assume we're using as pure of functions as possible. This means, ivory tower, there are no exceptions, but rather, a Result or Maybe is returned. So you're you're just changing this:

fs.readFileSync('arrayOfPeopleJSON.txt')
|> JSON.parse
|> filterHumans
|> mapNames
|> fixNames
Enter fullscreen mode Exit fullscreen mode

to this:

Promise.resolve('arrayOfPeopleJSON.txt')
.then( JSON.parse )
.then( filterHumans )
.then( mapNames )
.then( fixNames )
Enter fullscreen mode Exit fullscreen mode

... but then you have to ensure JSON.parse is actually:

const safeJSONParse = str => {
  try {
    const result = JSON.parse(str)
    return result
  } catch(error) {
    console.log("safeJSONParse str:", str, "error:", error)
    retun []
  }
}
Enter fullscreen mode Exit fullscreen mode

However, even though I screwed up return and called it retun in the function, the Promise just "handles" it fo me; no action on my part. That's the kind of stuff I know will happen eventually. It's the other stuff I don't that is quite complex like http/node-fetch responses that get super hard and complex. I'll start high level, like response => response.json(), but then break it down to handle statusCode, and various return values based on the API. Again, these functions are as pure as possible, and tend to return multiple values or a Result. The catch is always there to say "Yo dog, this is JavaScript, you missed one.... why aren't you using types?"

You can do that same style in async/await, no doubt, but I find people who do really don't care about pure functions, dependency injection, or any other FP style concepts. Again, though, you have to remember to put try/catches, whereas with Promise you don't.

The caveat with the above is in the browsers, I've seen some horrible things with window.onerror returning true. It's... it's really depressing. They'll basically create a global denylist, and say "we don't care about runtime exceptions, except for these 3". I know some love that power, but I'd prefer they endeavor to write more solid code. Yes, I get that's impossible in JavaScript, but you can make an effort and see improvements here; I believe it's worth doing.

The other caveat is Node.js, especially recent versions like 12 and 14. If you don't have a try/catch, or a catch on Promise changes, she'll crash your whole program, which I agree with. However, again, I've seen people do process.on for both sync and async exceptions and use that as a crutch rather than do the work to write more solid code. Again, that's nuanced, because some code bases are just... well, brutal. They might have been inherited or have 3rd party nastiness, so I get it. My empathy, however, doesn't condone that behavior.

Yeah, do in Haskell and let in Elm are "crutches" I use a lot. Despite practicing FP for years, my brain is still wired to think about hard problems imperatively (in sequential statements like you said). The let keyword helps immensely when I'm trying to reason about a problem in steps. I think the difference there, though, is:

  1. you have types so you can't screw it up, or the compiler won't compile your code and
  2. you're forced to handle Nothing or Failure scenarios where in JavaScript you can just "oh it's a happy path, if it fails, that's ok"

Ok, now you said the mutating word, it's too early in the morning... I can't go on. :: hugs immutability coffee cup ::