Events - for things that can happen many times.
Callbacks (or promises) - for things that can happen once.
So for example, when you have a function that you call because you need a current temperature taken from some API, that function should either return a promise or take a callback that can later be called with the right value (or error).
If, on the other hand, you have a function that you call because you need to get a new temperature every time when it changes, then this function should return an event emitter (or take an event handler to attach to some internal event emitter).
Now, the question on when to use callbacks and when to use promises is a little bit more tricky because they are good for the same kinds of situations - when you want to know a result of some asynchronous operation (some data or error). Since both work for the same situations let's consider two examples of reading the contents of a file.
First, with callbacks:
let fs = require('fs');
fs.readFile('a.txt', 'utf-8', (err, data) => {
if (err) {
console.log('Error:', err.message);
} else {
console.log('Data:', data.trim());
}
});
If there is no file it will print:
Error: ENOENT: no such file or directory, open 'a.txt'
If there is a file it will print:
Data: Contents of a.txt
Now, the same with promises:
let fs = require('mz/fs');
fs.readFile('b.txt', 'utf-8')
.then(data => {
console.log('Data:', data.trim());
})
.catch(err => {
console.log('Error:', err.message);
});
It works exactly the same as the previous example.
For that simple example the difference may not be very obvious but what if you wanted to have a function that abstracts some of that logic away.
For example this, with callbacks:
let fs = require('fs');
function a(cb) {
fs.readFile('b.txt', 'utf-8', (err, data) => {
if (err) {
return cb('a() error: ' + err.message);
}
cb(null, 'a() data: ' + data.trim());
});
}
a((err, data) => {
if (err) {
console.log('Error:', err);
} else {
console.log('Data:', data);
}
});
It will print either this
Error: a() error: ENOENT: no such file or directory, open 'a.txt'
or something like this:
Data: a() data: Contents of a.txt
Now, what is different with promises is that you can store it in a variable, return it from a function or pass it as an argument to some other function before attaching the success/error handlers. For example:
let fs = require('mz/fs');
function a() {
return fs.readFile('a.txt', 'utf-8')
.then(data => 'a() data: ' + data.trim())
.catch(err => Promise.reject('a() error: ' + err.message));
}
let promise = a();
promise.then(data => console.log('Data:', data))
.catch(err => console.log('Error:', err));
It works the same, it is written in a different style that you may or may not find more readable, but the difference is that now you don't have to attach a callback at the time of calling the a()
function. You can do it somewhere else.
If you didn't want to change the error message, it would be this with callbacks:
function a(cb) {
fs.readFile('a.txt', 'utf-8', (err, data) => {
if (err) {
return cb(err);
}
cb(null, 'a() data: ' + data.trim());
});
and this with promises:
function a() {
return fs.readFile('a.txt', 'utf-8')
.then(data => 'a() data: ' + data.trim());
}
Another difference is that if you have a function that returns a promise, you can use a new await
keyword inside of a async function
like this:
async function x() {
try {
console.log('Data:', await a());
} catch (err) {
console.log('Error:', err);
}
}
You cannot use await
with a function that doesn't return a promise.
It gets very convenient for example when you need to read file a.txt
to get another filename that it contains, and then read that other file and print its contents while handling all errors in more complex situations.
To use async
and await
with Node v7.x you need to use the --harmony
flag, see: