I'm not aware of a published implementation of this technique, but it's a fairly standard way of using promises. An interesting side effect of the composability, reusability, and extensibility of functional programming is that often you don't find things in libraries that you would expect.
In a less composable paradigm, you would need support in a library to do something like your throttle function, because you have to weave support for it throughout other functions. In functional code, programmers just write it themselves once, then they can reuse it everywhere.
After you write something like this, who do you share it with, and how? It's too small to be its own library. It's a little too specific to not be considered bloat in a promise library. Perhaps some sort of promise utilities library, but then it would be in a not very cohesive module with other functions vaguely related to promises, which makes it hard to find.
What happens to me a lot is I search for 10 minutes or so, then just write it myself because it would only take me a half hour. I put it in a "utilities" file I keep around with other odds and ends functions that are short but highly reusable, but don't really fit anywhere. Then six months later I happen upon it by chance in a community-maintained library with some weird name.
My point is, with functional programming, not being able to find an existing implementation for something that feels fairly "standard" is not a sign that other people are either doing it in a superior way, or that you're the first to come up with it. It's just a symptom of the difficulty of sharing short, reusable code in a way that's easy to discover by a search.
It is fair to say promises are just syntactic sugar. Everything you can do with promises you can do with callbacks. In fact, most promise implementations provide ways of converting between the two whenever you want.
The deep reason why promises are often better is that they're more composeable, which roughly means that combining multiple promises "just works", while combining multiple callbacks often doesn't. For instance, it's trivial to assign a promise to a variable and attach additional handlers to it later on, or even attach a handler to a large group of promises that gets executed only after all the promises resolve. While you can sort of emulate these things with callbacks, it takes a lot more code, is very hard to do correctly, and the end result is usually far less maintainable.
One of the biggest (and subtlest) ways promises gain their composability is by uniform handling of return values and uncaught exceptions. With callbacks, how an exception gets handled may depend entirely on which of the many nested callbacks threw it, and which of the functions taking callbacks has a try/catch in its implementation. With promises, you know that an exception which escapes one callback function will be caught and passed to the error handler you provided with .error()
or .catch()
.
For the example you gave of a single callback versus a single promise, it's true there's no significant difference. It's when you have a zillion callbacks versus a zillion promises that the promise-based code tends to look much nicer.
Here's an attempt at some hypothetical code written with promises and then with callbacks that should be just complex enough to give you some idea what I'm talking about.
With Promises:
createViewFilePage(fileDescriptor) {
getCurrentUser().then(function(user) {
return isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id);
}).then(function(isAuthorized) {
if(!isAuthorized) {
throw new Error('User not authorized to view this resource.'); // gets handled by the catch() at the end
}
return Promise.all([
loadUserFile(fileDescriptor.id),
getFileDownloadCount(fileDescriptor.id),
getCommentsOnFile(fileDescriptor.id),
]);
}).then(function(fileData) {
var fileContents = fileData[0];
var fileDownloads = fileData[1];
var fileComments = fileData[2];
fileTextAreaWidget.text = fileContents.toString();
commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
downloadCounter.value = fileDownloads;
if(fileDownloads > 100 || fileComments.length > 10) {
hotnessIndicator.visible = true;
}
}).catch(showAndLogErrorMessage);
}
With Callbacks:
createViewFilePage(fileDescriptor) {
setupWidgets(fileContents, fileDownloads, fileComments) {
fileTextAreaWidget.text = fileContents.toString();
commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
downloadCounter.value = fileDownloads;
if(fileDownloads > 100 || fileComments.length > 10) {
hotnessIndicator.visible = true;
}
}
getCurrentUser(function(error, user) {
if(error) { showAndLogErrorMessage(error); return; }
isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id, function(error, isAuthorized) {
if(error) { showAndLogErrorMessage(error); return; }
if(!isAuthorized) {
throw new Error('User not authorized to view this resource.'); // gets silently ignored, maybe?
}
var fileContents, fileDownloads, fileComments;
loadUserFile(fileDescriptor.id, function(error, result) {
if(error) { showAndLogErrorMessage(error); return; }
fileContents = result;
if(!!fileContents && !!fileDownloads && !!fileComments) {
setupWidgets(fileContents, fileDownloads, fileComments);
}
});
getFileDownloadCount(fileDescriptor.id, function(error, result) {
if(error) { showAndLogErrorMessage(error); return; }
fileDownloads = result;
if(!!fileContents && !!fileDownloads && !!fileComments) {
setupWidgets(fileContents, fileDownloads, fileComments);
}
});
getCommentsOnFile(fileDescriptor.id, function(error, result) {
if(error) { showAndLogErrorMessage(error); return; }
fileComments = result;
if(!!fileContents && !!fileDownloads && !!fileComments) {
setupWidgets(fileContents, fileDownloads, fileComments);
}
});
});
});
}
There might be some clever ways of reducing the code duplication in the callbacks version even without promises, but all the ones I can think of boil down to implementing something very promise-like.
Best Answer
Note that a function being
async
doesn't turn it into something completely different, it just enables some syntax. A normal function returning a promise is just as asynchronous as a function marked withasync
.The
await
serves as a warning sign "the world may have changed in the mean time".await
may be easier than multi threading, but it's still very error prone. During code review you have to look at eachawait
, thinking "does the function rely on any information obtained before theawait
still being the same? Is this guaranteed?". Without an explicitawait
, you have to do so at every function invocation.async
at every call to an externally defined function, just in case it returns a promise.if result is a promise then await
check at every method call.