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) or watch directly via vimeo and read the slides of the brilliant talk "Concurrency is not parallelism".
A Promise
has three states:
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:
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.
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.
A promise may be continued with a future action once it resolves or rejects.
promise.then()
is called once promise
resolvespromise.catch()
is called once promise
rejectspromise.finally()
is called once promise
either resolves or rejectsEvery 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);
"Success!"
after approximately 1000 ms.
promise1
will be resolved
and "Success!"
.promise2
will be resolved
and true
."NOPE!"
.
promise1
will be rejected
and Nope!
.promise2
will be resolved
and false
.It is important to understand that because of the rules of the lifecycle, when it reject
s, 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.
Sometimes you want to capture errors and only continue when the original promise reject
s.
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
.
promise1
resolves, catch
is skipped and it reaches then
, and logs the value.
promise1
will be resolved
and "Success!"
.promise2
will be resolved
and "done"
;promise1
rejects, catch
is executed, which returns a value, and thus the chain is now resolved
, and it reaches then
, and logs the value.
promise1
will be rejected
and "Nope!"
.promise2
will be resolved
and "done"
;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.
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');
});
randomNumber
is 0-3
:
myPromise
will be resolved with the value 2, 4, 6, or 8
finalPromise
will be resolved with the value 'yay'
Sampled data: ...
Promise completed
randomNumber
is 4-5
:
myPromise
will be rejected with the reason 'Sampling did not result in a sample'
finalPromise
will be rejected with the reason Error('Sampling did not result in a sample')
Promise completed
"uncaught rejected promise: Error('Sampling did not result in a sample')"
logAs 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 is a good resource to consume.
In this exercise, you'll be providing a TranslationService
that provides basic translation services to free members, and advanced translation to premium members with quality assurances.
The API
You have found an outer space translation API that fulfills any translation request
in a reasonable amount of time.
You want to capitalize on this.
The space translators are extremely fickle and hate redundancy, so they also provide API storage satellites where you can fetch
past translations without bothering them.
Fetching a translation
api.fetch(text)
fetches a translation of text
from the API storage and returns a promise
that provides two values:
translation
: the actual translationquality
: the quality expressed as a numberIf a translation is not found in the API storage, the API throws a NotAvailable
error.
Translations can be added using the api.request
method.
If 'text' is not translatable, the API throws an Untranslatable
error.
api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })
Requesting a translation
Some translations are sure to exist, but haven't been added to the API storage yet. That's the difference between NotAvailable
( not in storage, but can be requested ) and Untranslatable
( cannot be translated ).
api.request(text, callback)
requests that a translation of text
be performed and added into the API storage.
On completion the callback
function is called.
callback
is passed undefined
: this indicates the translation was successful and is accessible using the api.fetch
method.callback
is passed an error
: this indicates something went wrong.
The outspace API is unstable, which means that the API fails often.
If that happens, it is okay to api.request
again.api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
// because it was successful.
âš Warning! âš
The API works its magic by teleporting in the various translators when a request
comes in.
This is a very costly action, so it shouldn't be called when a translation is available.
Unfortunately, not everyone reads the manual, so there is a system in place to kick-out bad actors.
If an api.request
is called for text
is available, the API throws an AbusiveClientError
for this call, and every call after that.
Ensure that you never request a translation if something has already been translated.
The free service only provides translations that are currently in the API storage.
Implement a method free(text)
that provides free members with translation that already exist in the API storage.
Ignore the quality and forward any errors thrown by the API.
api.fetch
method (api.fetch
returns a promise
)service.free('jIyaj');
// => Promise<...> resolves "I understand."
service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")
Implement a method batch([text, text, ...])
for free members that translates an array of text using the free service, returning all the translations, or a single error.
BatchIsEmpty
error if no texts are givenservice.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]
service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")
service.batch([]);
// => Promise<...> rejects BatchIsEmpty()
Implement a premium user method request(text)
, that requests a translation be added to the API storage.
The request should automatically retry if a failure occurs.
It should perform no more than 3 calls for the same request (don't upset the space translators!!!).
api.request
does not return an error, resolve with undefined
api.request
returns an error, retry at most two timesservice.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API
Implement a premium user method premium(text, quality)
to fetch a translation.
If a translation is NotAvailable
, request the translation and fetch it after its been added to the API storage.
The method should only return the translation if it meets a certain quality
threshold.
api.fetch
resolves, check the quality before resolvingapi.fetch
rejects, request the translation insteadapi.request
rejects, forward the errorservice.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."
service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()
service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"
N.B.
The correct translation of 'arlogh Qoylu'pu'?
is How many times has it been heard?.
Sign up to Exercism to learn and master JavaScript with 35 concepts, 152 exercises, and real human mentoring, all for free.