Promises
Ringkasan Pelajaran
# Introduction
About
The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
This is a hard topic for many people, specially if you know programming in a language that is completely _synchronous_.
If you feel overwhelmed, or you would like to learn more about **concurrency** and **parallelism**, [watch (via go.dev)][talk-blog] or [watch directly via vimeo][talk-video] and [read the slides][talk-slides] of the brilliant talk "Concurrency is not parallelism".
[talk-slides]: https://go.dev/talks/2012/waza.slide#1
[talk-blog]: https://go.dev/blog/waza-talk
[talk-video]: https://vimeo.com/49718712
Lifecycle of a promise
A Promise has three states:
- pending
- fulfilled
- rejected
When it is created, a promise is pending. At some point in the future it may resolve or reject. Once a promise is resolved or rejected once, it can never be resolved or rejected again, nor can its state change.
In other words:
- When pending, a promise:
- may transition to either the fulfilled or rejected state.
- When fulfilled, a promise:
- must not transition to any other state.
- must have a value, which must not change.
- When rejected, a promise:
- must not transition to any other state.
- must have a reason, which must not change.
Resolving a promise
A promise may be resolved in various ways:
// Creates a promise that is immediately resolved
Promise.resolve(value);
// Creates a promise that is immediately resolved
new Promise((resolve) => {
resolve(value);
});
// Chaining a promise leads to a resolved promise
somePromise.then(() => {
// ...
return value;
});
In the examples above value can be anything, including an error, undefined, null or another promise.
Usually you want to resolve with a value that’s not an error.
Rejecting a promise
A promise may be rejected in various ways:
// Creates a promise that is immediately rejected
Promise.reject(reason)
// Creates a promise that is immediately rejected
new Promise((_, reject) {
reject(reason)
})
// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
// ...
throw reason
})
In the examples above reason can be anything, including an error, undefined or null.
Usually you want to reject with an error.
Chaining a promise
A promise may be continued with a future action once it resolves or rejects.
promise.then()is called oncepromiseresolvespromise.catch()is called oncepromiserejectspromise.finally()is called oncepromiseeither resolves or rejects
then
Every promise is “thenable”.
That means that there is a function then available that will be executed once the original promise is resolves.
Given promise.then(onResolved), the callback onResolved receives the value the original promise was resolved with.
This will always return a new “chained” promise.
Returning a value from then resolves the “chained” promise.
Throwing a reason in then rejects the “chained” promise.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"
return true;
});
This will log "Success!" after approximately 1000 ms.
The state & value of promise1 will be resolved and "Success!".
The state & value of promise2 will be resolved and true.
There is a second argument available that runs when the original promise rejects.
Given promise.then(onResolved, onRejected), the callback onResolved receives the value the original promise was resolved with, or the callback onRejected receives the reason the promise was rejected.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return true;
}
function shout(reason) {
console.error(reason.toUpperCase());
return false;
}
const promise2 = promise1.then(log, shout);
- In about 1/2 of the cases, this will log
"Success!"after approximately 1000 ms.- The state & value of
promise1will beresolvedand"Success!". - The state & value of
promise2will beresolvedandtrue.
- The state & value of
- In about 1/2 of the cases, this will immediately log
"NOPE!".- The state & value of
promise1will berejectedandNope!. - The state & value of
promise2will beresolvedandfalse.
- The state & value of
It is important to understand that because of the rules of the lifecycle, when it rejects, the resolve that comes in ~1000ms later is silently ignored, as the internal state cannot change once it has rejected or resolved.
It is important to understand that returning a value from a promise resolves it, and throwing a value rejects it.
When promise1 resolves and there is a chained onResolved: then(onResolved), then that follow-up is a new promise that can resolve or reject.
When promise1 rejects but there is a chained onRejected: then(, onRejected), then that follow-up is a new promise that can resolve or reject.
catch
Sometimes you want to capture errors and only continue when the original promise rejects.
Given promise.catch(onCatch), the callback onCatch receives the reason the original promise was rejected.
This will always return a new “chained” promise.
Returning a value from catch resolves the “chained” promise.
Throwing a reason in catch rejects the “chained” promise.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return 'done';
}
function recover(reason) {
console.error(reason.toUpperCase());
return 42;
}
const promise2 = promise1.catch(recover).then(log);
In about 1/2 of the cases, this will log "Success!" after approximately 1000 ms.
In the other 1/2 of the cases, this will immediately log 42.
- If
promise1resolves,catchis skipped and it reachesthen, and logs the value.- The state & value of
promise1will beresolvedand"Success!". - The state & value of
promise2will beresolvedand"done";
- The state & value of
- If
promise1rejects,catchis executed, which returns a value, and thus the chain is nowresolved, and it reachesthen, and logs the value.- The state & value of
promise1will berejectedand"Nope!". - The state & value of
promise2will beresolvedand"done";
- The state & value of
finally
Sometimes you want to execute code after a promise settles, regardless if the promise resolves or rejects.
Given promise.finally(onSettled), the callback onSettled receives nothing.
This will always return a new “chained” promise.
Returning a value from finally copies the status & value from the original promise, ignoring the value.
Throwing a reason in finally rejects the “chained” promise, overwriting any status & value or reason from the original promise.
Example
Various of the methods together:
const myPromise = new Promise(function (resolve, reject) {
const sampleData = [2, 4, 6, 8];
const randomNumber = Math.round(Math.random() * 5);
if (sampleData[randomNumber]) {
resolve(sampleData[randomNumber]);
} else {
reject('Sampling did not result in a sample');
}
});
const finalPromise = myPromise
.then(function (sampled) {
// If the random number was 0, 1, 2, or 3, this will be
// reached and the number 2, 4, 6, or 8 will be logged.
console.log(`Sampled data: ${sampled}`);
return 'yay';
})
.catch(function (reason) {
// If the random number was 4 or 5, this will be reached and
// reason will be "An error occurred". The entire chain will
// then reject with an Error with the reason as message.
throw new Error(reason);
})
.finally(function () {
// This will always log after either the sampled data is
// logged or the error is raised.
console.log('Promise completed');
});
- In the cases
randomNumberis0-3:myPromisewill be resolved with the value2, 4, 6, or 8finalPromisewill be resolved with the value'yay'- There will be two logs:
Sampled data: ...Promise completed
- In the cases
randomNumberis4-5:myPromisewill be rejected with the reason'Sampling did not result in a sample'finalPromisewill be rejected with the reasonError('Sampling did not result in a sample')- There will be one log:
Promise completed- in some environments this will yield an
"uncaught rejected promise: Error('Sampling did not result in a sample')"log
As shown above, reject works with a string, and a promise can also reject with an Error.
If chaining promises or general usage is unclear, the [tutorial on MDN][mdn-promises] is a good resource to consume.
[mdn-promises]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
Originally from Exercism javascript concepts