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:
- 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. - 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.
- 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.
- 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) )
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
}
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)
}
}
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)
}
}
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 ]) )
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 ]
}
}
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]
}
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) )
Top comments (20)
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 withasync await
. So what would it look like if we are using.then
and.catch
? How would you go about separating the error handling forsomeHttpCall
andfetchUserDetails
with justthen
andcatch
builtin in Promise?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:
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:
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.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 listeningsomeHttpCall()
will help you debug it: "These errors came from thesomeHttpCall
function, they did NOT even get to theresponse.json()
call." That alone can save you minutes to errors figuring things out. So something like: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:And many more:
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
, andresponse.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 readerror.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:Suddenly, instead of using a generic, but yes helpful, error:
We do the same thing, but with a slightly different constructor:
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:
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:
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: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 useinstanceof
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 aResult
. A Result that worked will be aResult.Ok
with some data in it. A Result that failed will be aResult.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 offetchUserDetails
, 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.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? LikeAbsolutely, 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.
hey thanks for the reply. Two followup questions if you don't mind
Result
type still relevent or we can just use typedError
instances.never
which is helpful in that it will throw something, but TypeScript doesn't havethrowable
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
andinterface
implementations that do help. For example, an Array in TypeScript will use likeArray<string>
or[string]
style syntax. That's helpful for Result because some of the libraries will doResult<boolean>
which is kind of cool.Either way, we're kind of at the limits of JavaScript here; deviating from
throw
andtry/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.
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):
In a typed language, they'd prefer something like:
However, you could later go:
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.
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
orTokenExpiredError
so you can do exhaustive pattern matching?Assuming all custom Error classes you make only extend the
Error
base class, then you can be confident ininstanceof
. If you're just doing regular JavaScript, the whole big if statements of: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:
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.
looks exactly like
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
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
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
andawait
and it's imperative looking codeIt 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.
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
orhttp failed
. This is calledResult
orEither
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
orFileDataParseFailure
orFileNotThere
orTimedOutTryingToReadFile
with options on how you react to each one of those. Again, these compose in the same way, but aren't aPromise.reject
, they're just a normalPromise.resolve
.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 callingPromise.catch
and have so big issue with justcatch
.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:
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 onnull | 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 ofa + b
I should useadd(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.
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.
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.
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.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
orMaybe
is returned. So you're you're just changing this:to this:
... but then you have to ensure
JSON.parse
is actually: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, likeresponse => 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. Thecatch
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:
Ok, now you said the
mutating
word, it's too early in the morning... I can't go on. :: hugs immutability coffee cup ::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!
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. aResult.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.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.
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
I wouldn't write the async/await version like that anyway. You're creating a problem for promise chaining to solve.
Code is meant for people, not computers. Computers don't have the capacity to care how succinct or neat code is; the entire purpose is for other humans to be able to read, maintain and modify it and why it matters at all is only insofar as people collaborate with each other on a project.
Async/Await has much better readability than promise chaining, the trick is to stick to the single-responsibility principle and not write functions that have so many points of failure. I would have written the code more like this (note: I used the actual fetch() method since that was implied by your code):
And really you could handle errors in a few different ways. The individual functions could always return something or null and then the master function could check for null and react accordingly. In theory you really should never need a try/catch because the types of errors that you have no control over in production (server/database errors that are outside of your code) don't cause faults on your code (after all, they're literally not part of your code) so you should instead be able to check the status code of the HTTP response header or the value that's returned and simply write conditional code that reacts to unwanted values (nulls). In my example, I might have each function return null on failure and then do some last-minute null handling on the master function that calls the others.
Composing functions is how Functional Programmers work, and there are many of them working in both JavaScript and their other languages. Given JavaScript is the crossroads of all styles, it's neat to see them adopt these features to help us get acclimated and use our existing styles. I know it may seem odd that people write code like that, but if you've ever worked in Elixir/Erlang, Scala, Haskell, OCAML/Reason/ReScript, or Elm, that's just "how it is".
Promises, assuming you only utilize the 1st parameter, which most in the wild I've seen do, only have 1 point of failure: the
catch
. However, you have the option of overriding that catch to return a resolved Promise, or the reverse, to return a rejected Promise in a resoling one. Either way, whoever calls it has the option to write 1 catch, regardless if there are 2 or 20 billion thens and catches. Async await does not have the advantage; you must implement a try/catch whereas Promise has try/catch built in. Additionally, try/catch is separate from from the async/await keywords, making it very imperative, and likely to miss something. TypeScript can help here a little bit by ensuring you returnnever
or a value likestring
, but TypeScript still doesn't have good forced checked exections.Unfortunately, SOLID is also nuanced, and there many valid counterpoints to smaller functions.
Regarding your code example, I know those take time so thanks for working through it. It is imperative in style, and that style is shared by many programmers, not just in Go or Rust, but also OOP languages and even FP languages. However, imperative is often avoided because of the lack of good types, and dangers in state and mutation bugs.
I'm unclear why you're ignoring errors and just logging them.