DEV Community

Jesse Warden
Jesse Warden

Posted on • Originally published at jessewarden.com

Why I Don't Use Async Await

A lot of JavaScript developers speak in exceptions. However, JavaScript does not have any defined practices on “good exception handling”. What does good mean? All using try/catch, .catch for Promises, and window.onerror in the browser or process.on for Node.js? Just http/file reading/writing calls? 3rd party/vendor systems? Code with known technical debt? None “because fast, dynamic language”?

In my view, good exception handling is no exceptions. This means both writing code to not throw Exceptions, nor cause them, and ensuring all exceptions are handled.

However, that’s nearly impossible in JavaScript as it is a dynamic language and without types, the language encourages the accidental creation of null pointers. You can adapt certain practices to prevent this.

One in the particular is not using async await.

A warning, this is a minority view, and only some functional languages hold this view. I also acknowledge my Functional Programming bias here. JavaScript accepts all types of coding styles, not just FP.

The Promise

Promises are great for a variety of reasons; here are 4:

  1. They have built-in exception handling. You can write dangerous code, and if an Exception occurs, it’ll catch it, and you can write a catch function on the promise to handle it.
  2. They are composable. In functional programming, you create pure functions, which are rad by themselves, and you wire them together into pipelines. This is how you do abstraction and create programs from functions.
  3. They accept both values and Promises. Whatever you return from the then, the Promise will put into the next then; this includes values or Promises, making them very flexible to compose together without worry about what types are coming out.
  4. You optionally define error handling in 1 place, a catch method at the end.
const fetchUser => firstName => 
  someHttpCall()
  .then( response => response.json() )
  .then( json => {
    const customers = json?.data?.customers ?? []
    return customers.filter( c => c.firstName === 'Jesse' )
  })
  .then( fetchUserDetails )
  .catch( error => console.log("http failed:", error) )
Enter fullscreen mode Exit fullscreen mode

However, they’re hard. Most programmers do not think in mathematical pipelines. Most (currently) think in imperative style.

Async Await

The async and await keywords were created to make Promises easier. You can imperative style code for asynchronous operations. Rewriting the above:

async function fetchUser(firstName) {
  const response = await someHttpCall()
  const json = await response.json()
  const customers = json?.data?.customers ?? []
  const user = customers.filter( c => c.firstName === 'Jesse' )
  const details = await fetchUserDetails(user)
  return details
}
Enter fullscreen mode Exit fullscreen mode

But there is a problem, there is no error handling. Let’s rewrite it with a try/catch:

async function fetchUser(firstName) {
  try {
    const response = await someHttpCall()
    const json = await response.json()
    const customers = json?.data?.customers ?? []
    const user = customers.filter( c => c.firstName === 'Jesse' )
    const details = await fetchUserDetails(user)
    return details
  } catch(error) {
    console.log("error:", error)
  }
}
Enter fullscreen mode Exit fullscreen mode

However, there are also some nuances. For example, we want to separate the error handling for someHttpCall and it’s data handling from fetchUserDetails.

async function fetchUser(firstName) {
  try {
    const response = await someHttpCall()
    const json = await response.json()
    const customers = json?.data?.customers ?? []
    const user = customers.filter( c => c.firstName === 'Jesse' )
    try {
      const details = await fetchUserDetails(user)
      return details
    } catch(fetchUserDetailsError) {
      console.log("fetching user details failed, user:", user, "error:", fetchUserDetailsError)
    }
  } catch(error) {
    console.log("error:", error)
  }
}
Enter fullscreen mode Exit fullscreen mode

This can get more nuanced. Now you have the same problem you have with nested if statements, it’s just quite hard to read. Some don’t view that as a problem.

Golang / Lua Style Error Handling

The Golang and Lua devs do view that as a problem. Instead of Exception handling like JavaScript/Python/Java/Ruby do, they changed it to returning multiple values from functions. Using this capability, they formed a convention of returning the error first and the data second. This means you can write imperative code, but no longer care about try/catch because your errors are values now. You do this by writing promises that never fail. We’ll return Array’s as it’s easier to give the variables whatever name you want. If you use Object, you’ll end up using const or let with the same name which can get confusing.

If you use traditional promises, it’d look like this:

const someHttpCall = () =>
  Promise.resolve(httpCall())
  .then( data => ([ undefined, data ]) )
  .catch( error => Promise.resolve([ error?.message, undefined ]) )
Enter fullscreen mode Exit fullscreen mode

If you are using async await, it’d look like this:

function someHttpCall() {
  try {
    const data = await httpCall()
    return [ undefined, data ]
  } catch(error) {
    return [ error?.message ] 
  }
} 
Enter fullscreen mode Exit fullscreen mode

If you do that to all your async functions, then when using your code, it now looks like this:

async function fetchUser(firstName) {
  let err, response, json, details
  [err, response] = await someHttpCall()
  if(err) {
    return [err]
  }

  [err, json] = await response.json()
  if(err) {
    return [err]
  }

  const customers = json?.data?.customers ?? []
  const user = customers.filter( c => c.firstName === 'Jesse' );
  [err, details] = await fetchUserDetails(user[0]);
  if(err) {
    return [err]
  }

  return [undefined, details]
}
Enter fullscreen mode Exit fullscreen mode

Then if all your functions look like this, there are no exceptions, and all functions agree to follow the same convention. This has some readability advantages and error handling advantages elaborated on elsewhere. Suffice to say, each line stops immediately without causing more errors, and secondly, the code reads extremely imperative from top to bottom which is preferable for some programmers.

The only issue here is not all errors are handled despite it looking like it. If you mispell something such as jsn instead of json or if you forget to wrap a function in this style like response.json, or just generally miss an exception, this style can only help you so much.

Additionally, you have to write a lot more code to put the error first, data last. The worse thing about this style is the constant checking if(err). You have to manually do that each time you call a function that could fail. This violates DRY pretty obnoxiously.

Conclusions

You know what doesn’t violate DRY, isn’t verbose, and handles all edge cases for exceptions, only requiring you to put exception handling in one place, but still remains composable?

Promises.

const fetchUser => firstName => 
  someHttpCall()
  .then( response => response.json() )
  .then( json => {
    const customers = json?.data?.customers ?? []
    return customers.filter( c => c.firstName === 'Jesse' )
  })
  .then( fetchUserDetails )
  .catch( error => console.log("http failed:", error) )
Enter fullscreen mode Exit fullscreen mode

Discussion (18)

Collapse
he_zhenghao profile image
Zhenghao He

Hi Jesse, just a quick question - you mentioned that if we want to separate the error handling for someHttpCall and it’s data handling from fetchUserDetails, we might end up writing nested try catch blocks with async await. So what would it look like if we are using .then and .catch? How would you go about separating the error handling for someHttpCall and fetchUserDetails with just then and catch builtin in Promise?

Collapse
jesterxl profile image
Jesse Warden Author

Sorry for the delay. It basically comes down to "What comes out of the then?"

There are basically 5 things you can do there, each with pro's and con's.

Helpful Error Messages

If we "embrace JavaScript", and also "embrace Promise chains" in the spirit of functional programming where your compose functions together, then the easiest, low-hanging fruit thing you can do is just more helpful error messages.

Let's take this block of code as an example:

 someHttpCall()
 .then( response => response.json() )
Enter fullscreen mode Exit fullscreen mode

As a back-end in engineer, there are a lot of things that could go wrong there, but as a front-end engineer, you really only care about 2 types of errors: If it's your fault, or the server's fault. So let's outline all the things that can go wrong:

  • "network problems": you can't make someHttpCall() work because you have no internet, internet but it's failing, internet but the server is having connection issues. This isn't your fault, BUT if you know this, then you can at least inform both yourself in the console, as well as the user (whether front-end browser or someone using your API) that your're having connection issues; maybe they could help by checking internet or VPN status.
  • "bad url": your http request, specifically the URL, is messed up. Either http vs. https, or you mispelled something, or are missing the dot com/net, etc. You can lump this under network problems, but this is technically your fault even if you're pulling this config from somewhere else, so your error message should be clear there IS something you can proactively do to fix the code, but not retry the request.
  • "timeout": If you're using retries or cancellable requests, while technically a "network problem", you CAN pay the cost of forcing the user to wait longer IF you're willing to retry. Sometimes there are blips and retries work which results in more likely to work code. This can be hard, I get it, but at least you know "My network is good, my URL is good, but I never heard back...."
  • "server mad": This could be anything from 100's, to 300's to 400's to 500's; basically "I didn't get my data back, the server is mad for some reason". You could say 400's are a client's fault, but server's aren't perfect; sometimes it's an invalid 400. Either way, if you think you're in the right, you need to log that you have a valid session so you can more easily debug with the server dev later.
  • "parsing issues" This is primarely what we'll be talking about below. Basically the only real thing that's our fault often; we attempted to parse something from the back-end, and it failed. Yes, they may have changed their API and not told us, or perhaps their JSON is f00barred, who knows, but bottom line, they claim they sent us data, but we couldn't understand it. This goes beyond just JSON.parse failing. There's a lot of work involved here.

So let's give an example of this. In that above code, you'll get all of those types of errors in your .catch. If the url is broken, if the server gives you a 500, if things timeout, etc., all will go to your catch. However, they're not always helpful. For example, simply throwing your own errors after listening someHttpCall() will help you debug it: "These errors came from the someHttpCall function, they did NOT even get to the response.json() call." That alone can save you minutes to errors figuring things out. So something like:

const getResponse = () =>
  someHttpCall()
  .then(
    response => {
     if(response.ok === false) {
        return Promise.reject(
          new Error(`We failed to make the http call, got a status code ${response.status} back, reason: ${response.statusText}`)
        )
     } else {
       return response
     }
   }
  )
Enter fullscreen mode Exit fullscreen mode

So that's a start and MUCH more helpful then a stack trace you have to wade through. Logging response is isn't really helpful because it's huge, has a lot of properties, and none really help you debug. This can go much deeper:

  • status code of 201 is fine, but 200 is wrong because it's a POST, what is going on
  • status code of 400 could actually be ok, maybe you just need to re-authenticate then try again? Is that possible? No?
  • status code of 429 could mean you just need to wait a few seconds and retry, negating the whole error

And many more:

  • if the response has headers, and those headers have a content type: response.headers.get('content-type') AND that type is json, then you're hitting an API that could send back errors as JSON, which some can do. However, there's no guarantee that will work. Do you care? Well, yeah. The server said it failed, but when we tried to get something beyond response.status and response.statusText, it sent back an unparseable error message? That clearly is something the server dev needs to fix.

You can see, all of this information is just generically thrown in a catch right now. You can invest the effort to make safe pure functions to process these responses and validate if they're good or not (e.g. if response.ok === true) and explain why they're not. Most of this is for you, but eventually it'll be the user (UI and/or API).

Take that example code I showed above inspecting response.ok, response.status, and response.headers.get('content-type') and put into tested pure functions. You can delete the tests after you're done since those are typically private functions, but it can help you learn.

Got it? Ok, let's talk custom.

Custom Error Messages

So the above is great, but super hard to take action on in your code. Meaning you'll post 12 different types of things that can go wrong, and you just have to look in 1 place: the .catch.

That's great, except your code still fails. Meaning, you'll get a failure, the code will log the error, you'll read the error in the logs, possibly fix some code, and redeploy. That really doesn't help the user much.

Additionally, if you're an orchestration API or a Back-end For Front-end calling other API's, you need to respond with some type of response http status code.

For example, if you're calling some microservice from your API, and it never responds or breaks somehow, you'll probably want to respond with a 504 or 500; "It's not my fault, it's some dude upstream I'm calling, but... I failed you, sorry; since they broke, we all broke."

How do you know that for sure? In "Better Error Messages", all you get is easier to quickly understand error messages. That means, in your catch, you can read error.message, and it's more helpful than normal. But that's too hard to do things with. Strings are powerful, but types are more powerful. Better to create custom error types so you can make decisions on some of them. For example, the simplest way is to simply create them:

class NetworkError extends Error {}
class TokenExpiredError extends Error{}
class ParsingError extends Error {}
class ThrottlingError extends Error {}
Enter fullscreen mode Exit fullscreen mode

Suddenly, instead of using a generic, but yes helpful, error:

return Promise.resolve(new Error("We got a 500 status code from the data microservice we're calling, they said 'network problem', but we couldn't parse their error message, not sure what's going on."))
Enter fullscreen mode Exit fullscreen mode

We do the same thing, but with a slightly different constructor:

return Promise.resolve(new NetworkError("..."))
Enter fullscreen mode Exit fullscreen mode

Same text message, but now it has a type. This allows us to intelligently do things based on what type of error message we get in the catch by pattern matching against it. If you use the standard JavaScript, it's:

 .catch(
  error => {
    if(error instanceof NetworkError) {
      return Promise.reject(error)
  } else if(error instanceof TokenExpiredError) {
    return attemptToReLogin.then(() => someHttpCall()) // <-- recursive retry
  } else if (error instanceof ThrottlingError) {
    return delay(1 * 1000).then(() => someHttpCall()) // <-- wait 1 second, then try again
  } else {
    // can't really recover from parsing error
    return Promise.reject(error)
  }
)
Enter fullscreen mode Exit fullscreen mode

Someday we'll be able to more powerfully pattern match on both the the request responses AND the errors, but until then, this'll at least make writing things in Express.js/Restify/Serverless a lot easier.

Go Lang Style Error Handling

The 3rd option is Go lang style of error handling where NO function ever throws, rather it returns if it worked or not. In JavaScript, that's:

let { ok, error, data } = await someHttpCall()
Enter fullscreen mode Exit fullscreen mode

Using await there is fine because someHttpCall will never throw; it has a try catch inside or a Promise you know won't reject; instead you return some value:

const someHttpCall = () =>
  fetch("url")
  .then( handleResponseOrReturnError )
  .catch(
    error =>
      ({ ok: false, error })
  )
Enter fullscreen mode Exit fullscreen mode

Using the let keyword, you can then write super imperative code like Go lang does to have async/await style imperative error handling, but completely bypass the need for try/catch. You'll still use instanceof to check for errors.

Result

The 4th way is more functional; it's like Go Lang, but you return a Result type out. JavaScript is pretty flexible, so there are a TON of ways to do this, each having pro's and con's. The easiest I've found is Folktale's Result. It even has a utility Result.try method for unsafe code. You basically do the same thing you did in the Go Lang style, but instead of returning an Object in the shape of { ok: true | false, data: "whatever", error: SomeErrorType }, you instead return a Result. A Result that worked will be a Result.Ok with some data in it. A Result that failed will be a Result.Error with a message in it. However, it may help to have a Result.Error actually be wrapped in a parent type so you can pattern match on the result like you did in the class type example. This is really hard to do in JavaScript alone; you need a library like Folktale that allows creating custom Union types. Check out the Folktale docs for Union, Quinn talks about custom errors for File failures in Node.js as an example: folktale.origamitower.com/api/v2.3...

Types

Using Result is cool, but SUPER hard in JavaScript because you start getting into strong types without a compiler to help you. Using the class NetworkError extends Error is easy because "they're all errors, they all arrive at the parent .catch, and we know which 4 we're getting so we can write instanceof". That doesn't really scale, though, in larger code that has MANY more functions with many things that can go wrong.

In larger code bases, where you have MANY types of Results that can blow up, it's better to type them, and used a typed langauge like TypeScript, ReScript, Elm, etc. That way, you know what type you're getting out. In TypeScript, you can pattern match using a switch statement. In ReScript & Elm, you can... pattern match on the errors. It allows error handling on a per function basis, and you can then wire them all together using Promises or pipes |> and the compiler will tell you what types of failures come out. MUCH more helpful.

That way, when the 4 errors of someHttpCall are wired up with the 4 additional errors that can come out of fetchUserDetails, your compiler will let you know which one of the 8 came out and give you an opportunity to do things for the ones you can know you can re-login or retry on.

Collapse
he_zhenghao profile image
Zhenghao He

wow thanks for the explanation! But I still don't quite understand when you said "you'll post 12 different types of things that can go wrong, and you just have to look in 1 place: the .catch." - wouldn't we be able to achieve the same thing with async/await as well? Like

async function fetchUser() {
  try {
    const user = await someHttpCall()
    const details = await fetchUserDetails(user)
    return displayUserDetails(details)
  } catch (error) {
    switch(error.name) {
      case 'HttpCallError':
        console.log('Networking error:', error)
        break

      case 'UserDetailsError':
        console.log('Fetching user error', error)
        break
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
jesterxl profile image
Jesse Warden Author

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 Author
  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.

Collapse
he_zhenghao profile image
Zhenghao He

also I am curious how you can distinguish the errors between bad url and bad internet? It seems really hard to tell them apart...
Also I am not super clear on the last section where you mentioned that "you know what type you're getting out" by type you meant the type for difference error e.g. NetworkError or TokenExpiredError so you can do exhaustive pattern matching?

Thread Thread
jesterxl profile image
Jesse Warden Author

Assuming all custom Error classes you make only extend the Error base class, then you can be confident in instanceof. If you're just doing regular JavaScript, the whole big if statements of:

if (thing instanceof OneClass) {
...
} else if (thing instanceof OtherClass) {
...
}
Enter fullscreen mode Exit fullscreen mode

That's fine; your name check works too. However, it's unsafe and you have to do by hand. Using something like TypeScript or ReScript will give you a compiler that can help you ensure you're:

  • spelling them right
  • ensuring your're checking for the correct properties
  • you've captured ALL possible errors
Collapse
captainn profile image
Kevin Newman

It doesn't look like your promises example has all the same error handling. Is this really comparing apples to apples?

Also, you could structure your try/catch blocks without nesting them, if you don't like that style.

Collapse
jesterxl profile image
Jesse Warden Author • Edited on

The point is you don't need to to have multiple try/catch blocks; just use 1 catch in a normal Promise. They all come out the same place. However, I acknowledge many prefer imperative style. This article is aimed at those who aren't sure why they're using that style, and if there are alternatives.

There is a path out. You start having each operation return something meaningful vs. "I don't know, nor care, just let all Exceptions bubble up and whoever is reading the catch will figure it out".

Once you've gotten accustomed to that, then you stop having errors, and instead start dealing with values like http worked or http failed. This is called Result or Either in other languages, but there are some neat ways to compose that.

Once you've gotten that concept down, you can then start returning multiple values such as FileData or FileDataParseFailure or FileNotThere or TimedOutTryingToReadFile with options on how you react to each one of those. Again, these compose in the same way, but aren't a Promise.reject, they're just a normal Promise.resolve.

Collapse
macsikora profile image
Pragmatic Maciej • Edited on

Jesse,
you are defining that one is imperative where the other is not. Guess what, to go to the.catch in the promise chain we need to throw an exception in the function in the chain 😀, so we still are imperative as we use exceptions but wrapped by Promise interface. Also the whole thing is imperative as we do http call in it directly. Funny is thought that you are feeling so good with calling Promise.catch and have so big issue with just catch.

In my personal experience in many languages, I see a lot of issue in the way of thinking which is presented in the article. People see some solution which work in language A, and force them to language B even though it doesn't work good enough in new environment. There are so many examples:

  • using HOF in Python (where in most we have list comprehensions)
  • using Maybe in languages with null article about that
  • trying to pretend there are no exceptions by wrapping errors in values in languages with exceptions as primary error flow syntax
  • using point free style in languages without currying
  • trying to avoid any statements like switch in statement based languages

All these things happens because somebody have seen pure FP language like Haskell, Elm and now tries to force this ideas into any other language. I am not saying ideas from these languages are wrong, I am saying there is a tradeoff, and in many situations tradeoff is negative. When we switch to Python for example there are Pythonic ways for solving staff, and "reduce" function is not one of them. When we switch to JS/TS we have exceptions, we have null values, trying to fight with them ends by introducing alien concepts to the language. For example the code in your example json?.data?.customers ?? [] is based on null | undefined values, and if we introduce Maybe we should not be using it. Still I am not saying that FP is wrong, we can still use FP, by avoiding side-effects, making pure functions, but it doesn't mean instead of a + b I should use add(a)(b) to be super composable FP king.

Back to exceptions. Yes they are problematic, yes they are outside of standard call stack, but they are there and they can be used with sense. Async/await syntax is great for readability with no comparison, code is flat, steps are clear, wrapping it by try catch makes us understand that anything bad will happen in the process, here is a place to cover this negative path. We can have flat code by creating custom exceptions and handling them in one place. This example with returning tuple [error, data] looks like nothing more than just having worst code (yes very opinionated statement 😉) only because I don't want to use standard error handling mechanism which is exactly exceptions.

I also was in that train, by overusing ramda, after that fp-ts, I had occasion to work with Elm, Haskell, and after all of these I just clearly see, taking an idea from one language to another is not always a good idea, something which is great in one place, doesn't need to be great in another.

Thread Thread
jesterxl profile image
Jesse Warden Author • Edited on

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 on

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 ::

Collapse
gitkearney profile image
Kearney Taaffe

Whoa! such a well written article. I actually hate this new syntax. I blame all the java developers who are coming to node and not being able to understand promises

I was reading the comments, and your definition of "imperative" vs functional is correct.

# IMPERATIVE
let x = someFunc();
let y = anotherFunc(x);
return y;
Enter fullscreen mode Exit fullscreen mode

looks exactly like

# LOOKS IMPERATIVE
let x = await someFunc();
let y = await anotherFunc(x);
return y;
Enter fullscreen mode Exit fullscreen mode

with promises you are EXPLICITLY communicating to other devs that the value of x will be passed to another function y and that someFunc returns

# ER MUH GRR! mah value iz being pazzed to a funk-shun!
someFunc()
  .then(x => anotherFunc(x))
  .then(y => {'ok': true, err: null, result: y})
  .catch(err => {'ok': false, err, result: null})
Enter fullscreen mode Exit fullscreen mode

However, I feel like you missed the most important thing when using promises vs await...DEBUGGING!!! As of a few months ago, it wasn't possible to add breakpoints to await statements using chrome, but with promises, it was easy by literally

someFunc()
  .then(x => {
    anotherFunc(x);
  })
  .then(y => {'ok': true, err: null, result: y})
  .catch(err => {'ok': false, err, result: null})
Enter fullscreen mode Exit fullscreen mode

now, this brings me to another problem, devs printing stuff out instead of using breakpoints :SMH but, that's another rant. I wanted to say you're not in the minority of not liking async and await and it's imperative looking code

Collapse
mjoycemilburn profile image
MartinJ

Please forgive my ignorance but I'd really appreciate advice on the nature of the exceptions you're aiming to catch.

I see plenty of exceptions thrown by my own rubbish code during Javascript and PHP development, but these get sorted out during testing. I'd have thought that Production systems should surely see only network exceptions.

However, I mostly find myself using Fetch() and here network error are returned as an HTTP error status. Accordingly, I wouldn't expect them to trigger a catch.

As a matter of fact, I do currently use the pure promise style rather than await and do attach a catch block at the end of my .then stacks. But I've yet to see one triggered!

I think I must be missing somthing and I'd really like to know what it is!

Collapse
jesterxl profile image
Jesse Warden Author • Edited on

Caveat: Most of this is with node-fetch in Node.js, not fetch in the browser. But similar things happen.

The first ones are like you said, during testing, like your parameters are messed up. You requested a person ID, but the person ID is null, so the entire function breaks.

The second ones are when you get back stuff that isn't JSON, so when response.json() runs, it breaks. At large companies this could be your firewall or WAF, or when the back-end freaks out and instead of sending JSON back, the nginx server sends an HTML error page.

The third ones are a mixup of the above. Sometimes you'll get a statusCode of 401 for example, but the response is in JSON giving you more details about it. However, your code gives back a bogus JSON response and that triggers other null pointers because they were expecting a good JSON response, not an error one.

The fourth is when all happy path is good, and the API changes something about the JSON; your fetch code works, but the code around it fails. You think it's the fetch' fault, but it's actually the API changing the schema. If you have schema validation (like ajv or something), this can help.

Occasionally I'll learn of a new one where my certs are wrong deployed to AWS and the fetch will throw "because https weirdness", or occasionally I'll get a 500 from the upstream and didn't handle the response correctly (text vs. JSON vs. html).

Those last ones are what kill me. The ones above I can convert all to pure functions, including data parsing where they never throw, just return a Result.Error vs. a Result.Ok. I like it better how in Elm has solidified it into 5 return values, and you can code to that interface, and you know exactly where the prolbem lies: with you or the back-end.

type Error
    = BadUrl String
    | Timeout
    | NetworkError
    | BadStatus Int
    | BadBody String
Enter fullscreen mode Exit fullscreen mode

It could just be you've worked with stable back-end systems, mine are either green field and changing, or have a lot of technical debt. Locally, things can be different because of strict proxy.

Collapse
mjoycemilburn profile image
MartinJ

Thanks Jesse, I really appreciate that. As you've detected, I'm working generally in stable (and low stress) areas but I've certainly sen the JSON problem often enough now and will concentrate on handling that one in future. Thanks again for your time and consideration, MJ