I am trying to implement a utility library in nodeJS that I can use in different projects. I am stuck with how to handle errors properly.
For example suppose I have a function
function dateCompare(date1,operator,date2) // it can take either a date object or valid date string.
if((date1,'eq',date2) || (date3,'l3',date4)
There's no "right" answer for questions like this. There are various different philosophies and you have to decide which one makes the most sense for you or for your context.
Here's my general scheme:
If you detect a serious programming mistake such as a required argument to a function is missing or is the wrong type, then I prefer to just throw an exception and spell out in the exception msg exactly what is wrong. This should get seen by the developer the first time this code is run and they should then known they need to correct their code immediately. The general idea here is that you want the developer to see their error immediately and throwing an exception is usually the fastest way to do so and you can put a useful message in the exception.
If there are expected error return values such as "user name already taken" or "user name contains invalid characters" that are not programming mistakes, but are just an indication of why a given operation (perhaps containing user data) did not complete, then I would craft return values from the function that communicate this info to the caller.
If your function needs to return either a result or an error, then you have to decide on a case by case basis if it is easy to come up with a range of error values that are easily detectable as separate from the successful return values. For example,
Array.prototype.indexOf() returns a negative value to indicate the value was not found or zero or a positive number to indicate it is returning an index. These ranges are completely independent so they are easy to code a test to distinguish them.
Another reason to throw an exception is that your code is likely to be used in a circumstance where it's simpler to let the exception propagate up several calling levels or block levels rather than manually writing code to propagate errors. This is a double edged sword. While sometimes it's very useful to let the exception propagate, sometimes you actually need to know about and deal with the exception at each level anyway to properly clean up in an error condition (release resources, etc...) so you can't let it go up that may levels automatically anyway.
If such a distinction is not simple to do for either you the code of the function or the developer who will call it, then sometimes it makes sense to return an object that has more than one property, one of which is an error property, another of which is a value.
In your specific case of:
if (dateCompare(date1,'eq',date2) || dateCompare(date3,'l3',date4))
It sure would be convenient if the function just returns a boolean and throws an exception of the date values or operator are invalid. Whether this is good design decision depends a bit on how this is going to be used. If you're in a tight loop, running this on lots of values, many of which will be badly formatted and would throw such an exception and performance is important in this case, then it may be better to return the above-described object and change how you write the calling code.
But, if a format failure is not a regular expected case or you're only doing it once or the performance difference of an exception vs. a return value wouldn't even be noticed (which is usually the case), then throw the exception - it's a clean way to handle invalid input without polluting the expected use case of the function.
How can we handle it so that it is easy to use as well as not very expensive?
It's not expensive to throw an exception upon bad input if that isn't the normally expected case. Plus, unless this code is in some kind of tight loop and called many times, it's unlikely you would even notice the difference between a return value and a thrown/caught exception. So, I'd suggest you code to make the expected cases simpler to code for and use exceptions for the unexpected conditions. Then, your expected code path doesn't go the exception route. In other words, exceptions actually are "exceptions" to normal.
Under what circumstances throwing exceptions will be more appropriate even if it is too expensive?
See the description above.