I advise to return a promise if the API might become asychronous one day. In this blog post I will advise to return a promise in case the api function returns an error. Returning the promise will allow the caller to handle the error in much cleaner way, even allowing the code up the calling stack to handle it.
Imagine you are designing a function that reads a file and returns its content. How will it handle
the "file does not exist" problem? It can throw an error, forcing its users to surround the call with
a try - catch
block.
1 | function readFile(filename) { |
My main problem with try - catch
block is that it is
NOT optimized by the JavaScript engine, leaving
the performance less than stellar. It is also boilerplate solution, adding a lot of source code.
We could return some error value, for example undefined
or null
1 | function readFile(filename) { |
I do not like this solution either. Whatever we pick: undefined
, null
or 0xBAD
could be a valid contents,
forcing us to pick more obscure values, or even pushing the API to check the value for us. For example, WIN32 apis
often contain a macro for checking if the handle / value returned by an API call is valid, see
HRESULT and
SUCCEEDED macro,
while the data contents is passed through a parameter.
pointer contents;
HRESULT result = Win32ReadFile(filename, contents);
if (!SUCCEEDED(result)) {
// ok, bad, contents is invalid
}
In JavaScript, a lot of APIs return -1 to signify an error.
1 | var contents = readFile('foo.js'); |
We could use a shortcut to compare a value with -1
(for example String.prototype.indexOf),
but I would strongly advise against it for clarity sake.
For example, this unit test
shows it is working, but in very unclear fashion.
1 | ~-1; // 0 |
Promise api
Instead, I prefer returning a promise. The best thing about the promises, is that you can upgrade the function
to be asynchronous later, and it has separate callbacks for success and failure. We can resolve with an
Error
instance in our API.
1 | function readFile(filename) { |
Notice that we:
- return immediately after rejecting, making sure we stop the
readFile
function. - Reject with a valid
Error
instance, and not just a string or an integer code. This ensures that the error has a valid stack.
The caller than can easily pipeline actions for success or failure
1 | readFile('foo.txt') |
Notice that in this case, the .catch
callback will be executed if the readFile
fails, and if there is
an error inside the success callback hasFile
. Just remember: every step in the promise chain executes
via the event loop queue, so interesting side effects are possible.