DEV Community

Discussion on: Why I Don't Use Async Await

 
jesterxl profile image
Jesse Warden

Absolutely, but that code is imperative. In functional programming, we don't create a bunch of variables which leads to accidental side effects, even if you are just in 1 function. We instead have a pure function, and it either returns a Result.Ok or a Result.Error. You then wire those together in 1 pure function, and that 1 pure function returns a Result.Ok or Result.Error. If you have a type system, you can break those various errors down into multiple types. It doesn't have to be 12, no doubt, I just mean, there possible 12 things that could go wrong, and maybe 4 of them you could react too and possibly retry/etc.

Again, nothing wrong the imperative example you showed; many like that and it works, and if you take the effort to throw more helpful, informative errors, that style can help you debug quickly too.

Thread Thread
 
he_zhenghao profile image
Zhenghao He

hey thanks for the reply. Two followup questions if you don't mind

  1. if we have TS or any other type system avaiable, is using the Result type still relevent or we can just use typed Error instances.
  2. "if you take the effort to throw more helpful, informative errors that style can help debug quickly too" please correct me if I am wrong but it sounds to me that you were implying that it would take more effort to throw helpful informative errros using imperative approach than the functional one. Can you maybe elaborate a little bit more on that?
Thread Thread
 
jesterxl profile image
Jesse Warden
  1. lol, that's a rabbit hole. If you're cool with imperative or OOP, it can help a lot, but most imperative/OOP developers don't return Errors from functions, they throw them. You can use never which is helpful in that it will throw something, but TypeScript doesn't have throwable like Java. So the compiler can help you a little bit and say "Oh man, this function doesn't return a value, you should probably wrap this with a try catch". And yeah, if you extend Error, then yeah, you'll get way more help from the compiler returning different types of Exceptions.

... but that doesn't really make sense. If you're willing to do the whole Exception dance, then you are cool with try/catch, and should just either only use it in exceptional circumstances like the OOP and imperative kids do.

If, however, you want to join the church of pure functions, then it's better to return a Result. The types in TypeScript are a bit tough, but I've seen a few type and interface implementations that do help. For example, an Array in TypeScript will use like Array<string> or [string] style syntax. That's helpful for Result because some of the libraries will do Result<boolean> which is kind of cool.

Either way, we're kind of at the limits of JavaScript here; deviating from throw and try/catch is not normal JavaScript. Doing Functional Programming in a non-functional language is hard, and you start getting into these weird situations where you "think of returning an Error from a function instead of throwing it" or "wrapping a failure in an Object and returning it". If you're a functional programmer, it all makes sense, AND when you're in a different land, you try to make it work. Not everything makes sense because the langauge doesn't make sense; it allows things that are "wrong"; wrong being against FP rules. Exceptions are in this weird middle ground; some people think exceptions in Haskell are ok; others don't. Either way, Result is kind of normalized in that world, so that's what we do.

Using types helps a TON once you start composing functions together, though. Even just regular promises using variadic tuples.

  1. Well... sort of. I think it's hugely worth spending the time to help. Like fetch is great, but you get this stack trace, and it's not always obvious why it failed. So spending the effort to be a bit more obvious will help you a ton when things start blowing up.

You still have these problems in functional languages, though. Like if you try to JSON.parse safely, with types, a response from the server and it fails, by default you might get "Failed to parse JSON." That is completely worthless and frustrating. Better to be "Attempted to parse a string firstName, and found a field called firstName, but it was null". NOW we're getting somewhere. So you can create crap errors or good errors; best to spend the effort. It's just in functional programming, it's wayyyyy more obvious where it came from: the function vs. "somewhere in the function". Although, when you start composing them together, that's not really helpful if you have a 30 function Promise chain, "Where in this chain did this error come from?".

However, I wouldn't say more effort, just that its' SUPER easy to screw up in imperative land. For example, this is ok in JavaScript (I'm not sure about TypeScript):

if(thing) {
  return true
}
return
Enter fullscreen mode Exit fullscreen mode

In a typed language, they'd prefer something like:

if(thing) {
  return true
}
return false
Enter fullscreen mode Exit fullscreen mode

However, you could later go:

if(thing) {
  return true
}
if(otherThing){
  return "something"
}
return false
Enter fullscreen mode Exit fullscreen mode

That kind of problem would never surface in something like Elm or ReScript; you're not allowed to omit the else, nor miss some of the elses, and the types must be the same. Imperative style code like that can get you into trouble. If you don't have those types of scenarios, then it makes it a lot easier to write more specific errors. If you go play in Elm or Haskell, you'll see what I mean. ReScript and F# are a bit more flexible, and you can run into the same types of problems where you have large functions that can do too many things.