Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Finally update the promise article to use callbacks.

  • Loading branch information...
commit 246cbb8e9d70f917de9667ec7e2ca61951a0ee44 1 parent 64b1eab
@creationix authored
View
144 articles/control-flow-part-ii.markdown
@@ -1,114 +1,57 @@
Title: Control Flow in Node Part II
Author: Tim Caswell
Date: Thu Feb 04 2010 02:24:35 GMT-0600 (CST)
+Node: v0.1.102
I had so much fun writing the last article on control flow, that I decided to play around with the feedback I received. One thing in particular I want to talk about is the good work [inimino][] is doing.
-Node has two constructs that are used currently to handle async return values, namely [promises and event emitters][]. You can read all about those on the [nodejs.org][] website. I'm going to talk about these and another way to manage asynchronous return values and streaming events.
+Node has two constructs that are used currently to handle async return values, namely callbacks and event emitters. You can read all about those on the [nodejs.org][] website. I'm going to talk about these and another way to manage asynchronous return values and streaming events.
-## Why the distinction between Promise and EventEmitter? ##
+**UPDATE** Promises were removed from node a while back, this article has been updated to show callbacks instead of promises. For promises see [node-promise][].
-In node there are two event handling classes. They are called Promise, and EventEmitter. Promises are for the async equivalent of a function.
+## Why the distinction between Callback and EventEmitter? ##
- var File = require('file');
- var promise = File.read('mydata.txt');
- promise.addCallback(function (text) {
- // Do something
- });
- promise.addErrback(function (err) {
- // Handle error
- })
+In node there are two event handling techniques. They are called callbacks, and EventEmitter. Callbacks are for the async equivalent of a function.
-File.read takes a filename and *returns* the contents of the file.
+<control-flow-part-ii/callback.js>
-Sometimes you want to listen for events that can happen several times. For example in a web server, when processing a web request, the `body` event is fired 1 or more times and then the `complete` event gets fired:
+`fs.readFile()` takes a filename and "*returns*" the contents of the file. It doesn't actually return it, but passes it to the passed in callback function.
- Http.createServer(function (req, res) {
- var body = "";
- req.addListener('body', function (chunk) {
- body += chunk;
- });
- req.addListener('complete', function () {
- // Do something with body text
- });
- }).listen(8080);
-
-The difference is that with a promise you're either going to get a success event or an error event. Never both, and never more than one event. For cases where there are more than two events and/or they can be called multiple times, then you need the more powerful EventEmitters.
-
-## Creating a custom promise ##
-
-Suppose I want to write a wrapper around `posix.open`, `posix.write`, and `posix.close`. Let's call it `fileWrite`. (This is extracted from the actual implementation of `File.write` in the "file" library.)
-
- function fileWrite (filename, data) {
- var promise = new events.Promise();
- posix.open(filename, "w", 0666)
- .addCallback(function (fd) {
- function doWrite (_data) {
- posix.write(fd, _data, 0, encoding)
- .addCallback(function (written) {
- if (written === _data.length) {
- posix.close(fd);
- promise.emitSuccess();
- } else {
- doWrite(_data.slice(written));
- }
- }).addErrback(function () {
- promise.emitError();
- });
- }
- doWrite(data);
- })
- .addErrback(function () {
- promise.emitError();
- });
- return promise;
- };
-
-And now this can be used as:
-
- fileWrite("MyBlog.txt", "Hello World").addCallback(function () {
- // It's done
- });
+Sometimes you want to listen for events that can happen several times. For example in a web server, when processing a web request, the `data` event is fired one or more times and then the `end` event gets fired:
+
+<control-flow-part-ii/http-body.js>
+
+The difference is that with a callback you're either going to get an error or a result. Never both, and never more than one event. For cases where there are more than two events and/or they can be called multiple times, then you need the more powerful and flexible EventEmitters.
+
+## The Node.js Callback style
-You'll note that we have to create a promise object, do our logic, and forward on the interesting results to the promise object.
+Node originally had promises instead of callbacks. Read the older versions of this article for more information. After much debate, node decided to drop Promises for simple callbacks.
+
+Any async function in node accepts a callback as it's last parameter. Most the functions in the `'fs'` module are like this. Then that callback is going to get the error (if any) as the first parameter.
+
+ // You call async functions like this.
+ someAsyncFunction(param1, param2, callback);
+
+ // And define your callback like this
+ function callback(err, result) {...}
## There could be another way ##
-Promises work well, but after reading about continuables from [inimino][], I was inspired to try another way.
+Promises worked well, but after reading about continuables from [inimino][], I was inspired to try another way.
-Remember our first example? Suppose that File.read was used like this:
+Remember our first example? Suppose that fs.readFile was used like this:
- var File = require('file');
- File.read('mydata.txt')(function (text) {
- // Do something
- }, function (error) {
- // Handle error
- });
+<control-flow-part-ii/continuable.js>
-Instead of returning a promise object, it returns a function that's expecting two callback methods: One for success and one for error. I call this the `Do` style, and you'll soon see why.
+Instead of expecting a callback, it returns a function that's expecting two callback methods: One for success and one for error. I call this the `Do` style, and you'll soon see why.
## Making callback style actions ##
-Often we will want to make custom functions that don't return a value right away. Using this new style, our `fileWrite` from above would look like this (assuming that the posix functions were converted to this style too):
-
- function fileWrite (filename, data) { return function (callback, errback) {
- posix.open(filename, "w", 0666)(function (fd) {
- function doWrite (_data) {
- posix.write(fd, _data, 0, encoding)(
- function (written) {
- if (written === _data.length) {
- posix.close(fd);
- callback();
- } else {
- doWrite(_data.slice(written));
- }
- }, errback);
- }
- doWrite(data);
- }, errback);
- }};
-
-Notice how easy it is to chain the error messages back up to our caller. Also this code is much shorter, and easier to read.
+Often we will want to make custom functions that don't return a value right away. Using this new style, let's make a `fileWrite` function that looks like this (assuming that the fs functions were converted to this style too):
+
+<control-flow-part-ii/file-write.js>
+
+Notice how easy it is to chain the error messages back up to our caller. Also this code is much shorter, and easier to read. (Than the original promise version, not the callback version)
The key to making these actions is to, instead of creating a promise and returning, return a function that takes two callbacks and then call them directly when needed.
@@ -120,23 +63,7 @@ I came up with a small library called `Do` earlier today. Actually it's not muc
Here is the entire library:
- Do = {
- parallel: function (fns) {
- var results = [],
- counter = fns.length;
- return function(callback, errback) {
- fns.forEach(function (fn, i) {
- fn(function (result) {
- results[i] = result;
- counter--;
- if (counter <= 0) {
- callback.apply(null, results);
- }
- }, errback);
- });
- }
- }
- };
+<control-flow-part-ii/do.js>
But combined with the callback style actions, this can lead to some very powerful and concise code.
@@ -214,7 +141,10 @@ Just for fun, here is the same example translated to the [Jack][] language (stil
| fun ->
# File was saved
+**UPDATE** I have since made a more powerful library that embraces node's native callback style called Step. Read more on it's article [step-of-conductor][].
+
[Jack]: http://github.com/creationix/jack
[inimino]: http://inimino.org/~inimino/blog/fileio_first_release
-[promises and event emitters]: http://nodejs.org/api.html#_events
+[node-promise]: http://github.com/kriszyp/node-promise
[nodejs.org]: http://nodejs.org/
+[step-of-conductor]: /step-of-conductor
View
11 articles/control-flow-part-ii/callback.js
@@ -0,0 +1,11 @@
+var fs = require('fs');
+
+fs.readFile('mydata.txt', function (err, buffer) {
+ if (err) {
+ // Handle error
+ console.error(err.stack);
+ return;
+ }
+ // Do something
+ console.log(buffer);
+});
View
23 articles/control-flow-part-ii/continuable-style-fs.js
@@ -0,0 +1,23 @@
+var fs = require('fs');
+var newFs = module.exports = {};
+
+Object.keys(fs).forEach(function (name) {
+
+ // Ignore the Sync and Stream functions
+ if (name.indexOf('Sync') >= 0 || name.indexOf('Stream') >= 0) {
+ return;
+ }
+
+ // Wrap the other ones
+ newFs[name] = function () {
+ var args = Array.prototype.slice.call(arguments);
+ return function (callback, errback) {
+ args.push(function (err, result) {
+ if (err) errback(err);
+ else callback(result);
+ });
+ fs[name].apply(fs, args);
+ }
+ };
+
+});
View
9 articles/control-flow-part-ii/continuable.js
@@ -0,0 +1,9 @@
+var fs = require('./continuable-style-fs');
+
+fs.readFile('mydata.txt')(function (text) {
+ // Do something
+ console.log(text);
+}, function (error) {
+ // Handle error
+ throw error
+});
View
19 articles/control-flow-part-ii/do.js
@@ -0,0 +1,19 @@
+var Do = {
+ parallel: function (fns) {
+ var results = [],
+ counter = fns.length;
+ return function(callback, errback) {
+ fns.forEach(function (fn, i) {
+ fn(function (result) {
+ results[i] = result;
+ counter--;
+ if (counter <= 0) {
+ callback.apply(null, results);
+ }
+ }, errback);
+ });
+ }
+ }
+};
+
+module.exports = Do;
View
30 articles/control-flow-part-ii/file-write.js
@@ -0,0 +1,30 @@
+var fs = require('./continuable-style-fs');
+
+function fileWrite (filename, data) { return function (callback, errback) {
+ fs.open(filename, "w", 0666)(function (fd) {
+ var totalWritten = 0;
+ function doWrite (_data) {
+ fs.write(fd, _data, 0, 'utf8')(
+ function (written) {
+ totalWritten += written
+ if (totalWritten === _data.length) {
+ fs.close(fd);
+ callback(totalWritten);
+ } else {
+ doWrite(_data.slice(totalWritten));
+ }
+ }, errback);
+ }
+ doWrite(data);
+ }, errback);
+}}
+
+// Use it!
+fileWrite('test', "Hello")(
+ function (written) {
+ console.log(written + " bytes successfully written");
+ },
+ function (err) {
+ throw err;
+ }
+);
View
14 articles/control-flow-part-ii/http-body.js
@@ -0,0 +1,14 @@
+var http = require('http');
+
+http.createServer(function (req, res) {
+ var chunks = [];
+ req.addListener('data', function (chunk) {
+ chunks.push(chunk);
+ });
+ req.addListener('end', function () {
+ // Do something with body text
+ console.dir(chunks);
+ });
+}).listen(8080);
+
+console.log("Server started on http://localhost:8080/");
Please sign in to comment.
Something went wrong with that request. Please try again.