Take the small "Hello World" Node webserver from nodejs.org home page as a starting point.
1 | var http = require('http'); |
Imagine we need to return a text file instead of "Hello World". We can read the file asynchronously to follow the node's way.
1 | var http = require('http'); |
How does the onRequest
function know when the hello
variable has data?
Well, we could check periodically
1 | http.createServer(function onRequest(req, res) { |
What if we want to return hello
if it is available without setting an interval?
1 | var hello; |
What if we want to load a file on the first request for it?
1 | var hello; |
Finally, what happens if there is second request for hello
while we are loading it?
It need to somehow tell other requests to wait until hello
is ready. We can use
a timer interval again, but we need to know if someone is loading the hello.txt already
1 | var hello, helloIsLoading; |
We now have 2 problems:
- We need to use 2 variables to track the data and the state of the data:
hello
andhelloIsLoading
. They are dependent on each other: ifhelloIsLoading
is true, then we assumehello
is null, etc. They might get out of sync really quickly. - The loading and waiting logic is already complex, and will only become more complicated as we introduce error handling and multiple resources.
Promises to the rescue
Promises offer an elegant solution to async resoure loading problem. Instead of hello
holding the data, it will hold a promise to load data. Promises resolve only once, every call
afterwards returns the resolved data. I will use my favorite promise implementation library
Q.
1 | var Q = require('q'); |
The very first request will call fs.readFile
, creating the promise to read a file
and return its content. The promise is stored in hello
variable (line // 1
).
The same first request and every request arriving afterwards will wait while the promise
has not been resolved (line // 2
). Once the file is read, its content will be the first
argument to the writeHello
function. The Q()
function is a shortcut for
Q.when
that accepts a value or a promise to that resolves with a value, and is similar to jQuery's
$.when() function.
Any request that arrives after that will check the hello
promise, see that it has been
resolved and will execute writeHello
without waiting.
Cache of promises
The above example showed a single resource loaded on demand using promises. Here is a more realistic example with multiple resources loaded on demand. Let us imagine we need to load multiple text files and cache them. This is how our file cache could look without promises
1 | var cache = { |
Instead of keeping the file source, we are going to keep promise to load the file.
1 | var cache = { |
Here is how we are going to do this on demand
1 | // cache is empty at first |
The key to understand this code is to notice that at first load the promise will be placed into the cache. If someone requests same file, the new request will be chained and will be resolved at the same time as first one. Any request after that will get the resolved promise, resolving on the next turn of the event loop.
You can even replace the first promise after resolving it with actual file contents, and the code will still work
1 | function readFile(filename) { |
I prefer the later code because it allows simpler cache inspection.
Conclusion
The promise offer an elegant and beautiful solution by removing the extra code and variables. You can see this approach in action in my proud-connect source that calculates metrics and generates a badge image on demand, while caching previously generated results.