Why bother capturing a thrown exception only to pass it up the call stack by return value so the caller has to explicitly check for it when you could use much simpler conventional exceptions mechanics?
functionelse_throw(fn,msg){returnasync(...args)=>fn(...args).catch(e=>Error(`${msg} caused by: ${e}`));}const_gu=getUser;getUser=else_throw(_gu,'Could not fetch user details');const_gfou=getFriendsOfUser;getFriendsOfUser=else_throw(_gfou,"Could not fetch user's friends");const_gup=getUsersPosts;getUsersPosts=else_throw(_gup,"Could not fetch user's posts");asyncfunctionuserProfile(){constuser=awaitgetUser();returnshowUserProfilePage(user,awaitgetFriendsOfUser(user),awaitgetUsersPosts(user));}
You are returning a promise in your else_throw function, which means you still need to catch/handle promise rejections with a .catch method or try...catch block inside of userProfile function
The use-case below will throw Unhandled promise rejection in any JS environment
functionelse_throw(fn,msg){returnasync(...args)=>fn(...args).catch(e=>Error(`${msg} caused by: ${e}`));}functiongetUser(){returnnewPromise((resolve,reject)=>{reject(newError('An error occured'));});}asyncfunctiontest(){let_gu=getUser;let_getUser=else_throw(_gu,'Could not fetch user details');console.log(awaitgetUser());}test();
The original example also allows errors to propagate from userProfile, but that's not the point of your post. Your post is about adding context to async errors. You would want to handle it at the call site to userProfile. You'd handle it in test(). The same is true of the code from the post.
The original example also allows errors to propagate from userProfile, but that's not the point of your post.
It's just an example, you can decide to return a http response assuming it's an API server. Also propagating error is not bad else an error from a node library will return an error as a return value instead of throwing, which could cause a lot of issues in your program.
Your post is about adding context to async errors. You would want to handle it at the call site to userProfile.
Assuming user fails with a rejection, user variable will be assigned an Error object. Without the user data whats the point of calling getFriendsOfUser(user) and getUsersPosts(user) with an Error object?
In scenarios like this throwing or returning a response will be a better way to deal with the resulting error from getUser request than to pass it to another function to deal with it.
Lets say you pass user which contains an Error object to getFriendsOfUser(user) it'll result to an error inside of getFriendsOfUser function since getFriendsOfUser is expecting a user object and not an Error object, so instead of your program to throw an error about user details not found, it'll probably cause a totally different error inside of getFriendsOfUser. Imagine scenarios like this all over your codebase, it'll be difficult to debug.
If you intend to handle errors when userProfile is called, then you'll need to check results from your await calls if its an instance of Error, then you'll also check if return value of userProfile() is an instance of Error then do whatever with the Error object.
I'm in favour of handling errors where they occur and/or propagating to the top.
Assuming user fails with a
rejection, user variable will
be assigned an Error object.
Without the user data whats the
point of calling
getFriendsOfUser(user) and
getUsersPosts(user) with an
Error object?
user will not be assigned to an instance of Error and getFriendsOfUser() will not be called in the case where getUser() rejects because await will cause the program to throw out of userProfile(). The error will propagate upward from the awaiting function and will have a message indicating the getUser() call failed.
I would caution you against handling all errors where they occur. You're not necessarily wrong to do that, but be careful not to use that as a hard and fast rule everywhere. Errors should be handled only where the program should take a specific action. This will vary on a case by case basis. In this case, the specific action is to swap one error for another, (but the error is not otherwise handled).
Before I make my next point, I want to say that I like what you're trying to do and I appreciate that you are giving serious thought to patterns that implement exception policy. I wish more devs did that. Don't stop thinking about ways to abstract away and standardize exception handling behavior because every program has that problem.
My code and yours more or less do the same two things: replaces a low level error with another containing context about userProfile()'s intentions; and causes early exit of userProfile(). What's true of one program's error handling behavior is true of the other in terms of outcome.
The caller does not have to check the return value of userProfile() because it's using conventional error propagation paths. This means the caller could use try/catch within an async function that awaits the call to userProfile() or by using .catch() on the promise returned by userProfile(). The caller's option to not handle the error and allow the error to continue propagating normally is also preserved.
The main problem with your proposal is that it defeats a major benefit of using async function, which is being able to write async code that is not cluttered by explicit error handling.
This benefit can be seen in my version because the body of userProfile() contains no explicit error handling, contains no branching (objectively less complex code), and the errors propagated say what userProfile() was doing at the time of the error.
My version could arguably be improved by creating the decorated versions of getUser() (and the others) in the body of userProfile() instead of at program initialization and adding some dynamic context to the error messages (like user name or whatever is useful to capture in the log).
When I tested your else_throw helper with a promise rejection, it returned and error object instead of throwing, which was the reason for my response. Unless I didn't test properly.
If it throws as expected then your approach will be a great alternative.
Why bother capturing a thrown exception only to pass it up the call stack by return value so the caller has to explicitly check for it when you could use much simpler conventional exceptions mechanics?
You are returning a promise in your
else_throw
function, which means you still need to catch/handle promise rejections with a.catch
method or try...catch block inside ofuserProfile
functionThe use-case below will throw
Unhandled promise rejection
in any JS environmentThanks for your feedback.
You have a bug on line:
Change to:
The original example also allows errors to propagate from
userProfile
, but that's not the point of your post. Your post is about adding context to async errors. You would want to handle it at the call site touserProfile
. You'd handle it intest()
. The same is true of the code from the post.Thank you for pointing the bug out.
It's just an example, you can decide to return a http response assuming it's an API server. Also propagating error is not bad else an error from a node library will return an error as a return value instead of throwing, which could cause a lot of issues in your program.
From your example
On this line
const user = await getUser();
Assuming
user
fails with a rejection,user
variable will be assigned anError
object. Without the user data whats the point of callinggetFriendsOfUser(user)
andgetUsersPosts(user)
with an Error object?In scenarios like this
throwing
or returning a response will be a better way to deal with the resulting error fromgetUser
request than to pass it to another function to deal with it.Lets say you pass
user
which contains anError
object togetFriendsOfUser(user)
it'll result to an error inside ofgetFriendsOfUser
function sincegetFriendsOfUser
is expecting a user object and not anError
object, so instead of your program to throw an error aboutuser details not found
, it'll probably cause a totally different error inside ofgetFriendsOfUser
. Imagine scenarios like this all over your codebase, it'll be difficult to debug.If you intend to handle errors when
userProfile
is called, then you'll need to check results from yourawait
calls if its an instance ofError
, then you'll also check if return value ofuserProfile()
is an instance ofError
then do whatever with theError
object.I'm in favour of handling errors where they occur and/or propagating to the top.
user
will not be assigned to an instance ofError
andgetFriendsOfUser()
will not be called in the case wheregetUser()
rejects becauseawait
will cause the program to throw out ofuserProfile()
. The error will propagate upward from theawait
ing function and will have a message indicating thegetUser()
call failed.I would caution you against handling all errors where they occur. You're not necessarily wrong to do that, but be careful not to use that as a hard and fast rule everywhere. Errors should be handled only where the program should take a specific action. This will vary on a case by case basis. In this case, the specific action is to swap one error for another, (but the error is not otherwise handled).
Before I make my next point, I want to say that I like what you're trying to do and I appreciate that you are giving serious thought to patterns that implement exception policy. I wish more devs did that. Don't stop thinking about ways to abstract away and standardize exception handling behavior because every program has that problem.
My code and yours more or less do the same two things: replaces a low level error with another containing context about
userProfile()
's intentions; and causes early exit ofuserProfile()
. What's true of one program's error handling behavior is true of the other in terms of outcome.The caller does not have to check the return value of
userProfile()
because it's using conventional error propagation paths. This means the caller could usetry
/catch
within anasync function
thatawait
s the call touserProfile()
or by using.catch()
on the promise returned byuserProfile()
. The caller's option to not handle the error and allow the error to continue propagating normally is also preserved.The main problem with your proposal is that it defeats a major benefit of using
async function
, which is being able to write async code that is not cluttered by explicit error handling.This benefit can be seen in my version because the body of
userProfile()
contains no explicit error handling, contains no branching (objectively less complex code), and the errors propagated say whatuserProfile()
was doing at the time of the error.My version could arguably be improved by creating the decorated versions of
getUser()
(and the others) in the body ofuserProfile()
instead of at program initialization and adding some dynamic context to the error messages (like user name or whatever is useful to capture in the log).Your approach is also good.
I understand your intention from this response.
When I tested your
else_throw
helper with a promise rejection, it returned and error object instead of throwing, which was the reason for my response. Unless I didn't test properly.If it throws as expected then your approach will be a great alternative.
I must have forgotten to have it throw from the
.catch()
callback. I intended for it to throw.Yeah. That should be it.