Understanding Javascript Promise Generation & Behavior
Explain the behavior of promises under multiple conditions. Build my own promise library
In the past, we did regular async. e.g.
client.query(query, callback) {
if (err) return next(err);
res.json(data.rows)
}
Sometimes this is called the continuation-passing model
When we use Page.findOne
in Sequelize, we're doing this same process.
We don't have Promises just to avoid callback hell!
In Promises we can:
- separate the async request from the eventual behavior that we want to run.
- pass the Promise around to other modules (portable)
- Multiple handlers in linear/flat chains (not callback hell)
- collect Promises into an array and pass them into a function
- Unified error handling (.catch), rather than in every callback
"A promise represents the eventual result of an asynchronous operation."
Three states of promises:
- Pending (contains nothing)
- Fulfilled (contains value from promise)
- Rejected (contains error)
Promises are POJO! Plain Old Javascript Objects :-)
state(pending, fulfilled, or rejected) // hidden if possible
information(value or a reason) // hidden if possible
.then() // <== a public property!
Remember, promises only change state while pending. If a promise is fulfilled or rejected, it can't go back to pending.
Promises/A+ is the standard that won. ES6 are a superset of this standard.
Don't forget, Promises are not a fundamental type like a string or a boolean. They're implemented.
// Fake solution to Promise implementation
const containerA = new Container();
asyncGetData(function (data) {
containerA.save( data ); // once async completes
});
// later...
containA.whenSaved( function ( data ) {
console.log( data ); // once containerA.save() happens
}
We are just putting a value into a box!
So how do we make new Promises?
const promiseforTxt = new Promise(function (resolve, reject) {
// Executor function takes a func with resolve and reject
fs.readFile('path.txt', function (err,text) {
if (err) reject( err ); // if we reject, we put an error in our Promise box (Rejected with error)
else resolve( text ); // if we resolve, we put a value into the box! (Fulfilled with value)
});
};
promiseForTxt.then( someSuccessHandler, someErrorHandler );
WHAT?! We can call it before or after the promise has resolved. This is like 'deciding' what you'll do with the value of the promise before it has resolved.
Calling .then on same promise
A single promise can have .then called on it at various points in your code, including before the promise was resolved.
When that promise is resolved, all the .then
s are resolved.
var promiseOne = promisifiedReadFile('text-one.txt');
promiseOne.then(function(val) {
console.log(val, 'logged from first .then()');
});
// While promise is processing, we have added a .then() to it
promiseOne.then(function(val) {
console.log(val, 'logged from second .then()');
});
// While promise is processing, we added ANOTHER .then() to it
/* We will see our first log then our second log because the `.then`s will be resolved in the order that we added them.
.then() is grabbing the value from the promise */
var promiseOne = promisifiedReadFile('text-one.txt');
promiseOne.then(function(val) {
console.log(val, 'logged from first .then()');
});
// While promise is processing, we have added a .then() to it
promiseOne.then(function(val) {
console.log(val, 'logged from second .then()');
});
setTimeout(function() {
promiseOne.then(green)}, 3000
});
/* Crazy! Will log our text file in green after a three second delay. */
This is why we can chain .then()
.
So what happens if we return in a .then...
const promiseB = promiseA.then(function thingSuccess(thing) {
return thingB;
})
If there is no handler... If there is no success handler, your .then looks for the first available success handler. If there is no failure handler, your .then looks for the first available failure handler.
Success and failure bubbles/trickles down through the .then
chain until it finds something to handle it.
If we reach a success handler with a value from a resolved Promise, we resolve the next promise with the value (result) from before.
If we return a totally new promise, then the next Promise that comes in that chain IS that promise.
promisifiedReadFile('text-one.txt')
.then() // No success handler, keep going.
.then(function(value) {
return 'I am the new value now!'; // We replace our old value with the returned one here.
}).then(function(value) {
console.log(value); // Logs 'I am the new value now!'
});
promisifiedReadFile('text-one.txt')
.then(function(value) {
console.log(value); // Logs my text file
// if I want the logging to continue I would have to return value here
}).then(function(value) {
console.log(value); // Logs nothing, because the value is undefined
}).then(function(value) {
console.log(value); // Logs nothing, because the value is undefined
}).then(function(value) {
console.log(value); // Logs nothing, because the value is undefined
}).then(function(value) {
console.log(value); // Logs nothing, because the value is undefined
})
promisifiedReadFile('text-one.txt')
.then(function(value) {
console.log(value);
return promisifiedReadFile('text-two.txt'); // We return a new promise with the text of our second file
}).then(function(value) {
console.log(value); // Logs our second text file
});
** If the promise rejects with a reason ** The value for error will be the error we threw in our last function.
This will look for an error handler in our .then chain. Once the error is dealt with, the .then chain will return to the success track.
promisifiedReadFile('this-file-doesnt-exist.txt')
.then(function(value) {
console.log(value);
return promisifiedReadFile('text-two.txt'); // No error handler!
}).then(function(value) {
console.log(value); // No error handler!
}).then(function(value) {
console.log(value); // No error handler!
}).then(function(value) {
console.log(value); // No error handler!
}).then(null, function(err) {
console.error(err); // Logs our error!
});