Promise Vs Observable
You may often hear that almost everything in JavaScript is non-blocking (asynchronous).
For us to understand that, we need to first talk about how JavaScript events work.
JavaScript is a single threaded language, meaning that only one thing can be run at a time. Some other languages are multi-threaded, which means they can run multiple processes at once.
Because JavaScript is single threaded, which means we can only run one thing at a time.
JavaScript is asynchronous.
What that means is that JavaScript won't stop running that code, instead if will put it off in what we call the web API, and when that comes back after sometime, it is going to stick it in the callback queue.
What are callbacks?
Callbacks are just the name of a convention for using JavaScript functions. There isn't a special thing called a 'callback' in the JavaScript language, it's just a convention. Instead of immediately returning some result like most functions, functions that use callbacks take some time to produce a result. The word 'asynchronous', aka 'async' just means 'takes some time' or 'happens in the future, not right now'. Usually callbacks are only used when doing I/O, e.g. downloading things, reading files, talking to databases, etc.
What is callback hell?
Callback Hell, also known as Pyramid of Doom, is an anti-pattern seen in code of asynchronous programming.
Asynchronous JavaScript, or JavaScript that uses callbacks, is hard to get right intuitively.
Callback hell is caused by poor coding practices. Luckily writing better code isn't that hard!
Promises
Nested async calls often result in a "callback pyramid of doom":
fetchData("/endpoint1", (result1) => {
fetchData("/endpoint2", (result2) => {
fetchData("/endpoint3", (result3) => {
// do something with results 1 through 3
})
})
})
This also makes error handling very difficult.
The JS Promise data type provides a structured way to handle future async results (slides: promise basics. Promises are objects can be in one of three states:
- Pending: created, but there is no result yet
- Fulfilled: completed, with a positive/successful result
- Rejected: completed, with an error result
A promise is an object that may produce a single value some time in the future: either a resolved value, or a reason that it’s not resolved.
A Promise has settled or resolved once it is either fulfilled or rejected.
Promises can be chained using somePromise.then(callback) and somePromise.catch(callback). When somePromise resolves, any provided chained callbacks will be run - .then() callbacks if it fulfilled, or .catch() callbacks if it rejected. Callbacks can even be chained after the promise has resolved, in which case they will be executed almost immediately.
Promise chains effectively form a pipeline. Each .then() or .catch() returns a new promise.
let promise1 = new Promise( (resolve, reject) => {
// This callback executes synchronously, immediately
// Could "resolve" the promise with a value:
resolve("a");
})
promise1
.then( (firstValue) => { // "a"
return "b";
})
.then( (secondValue) => { // "b"
// _Not_ returning a value returns `undefined`
})
.then( (thirdValue) => { // undefined
})
Inside a promise callback, you can run whatever calculations you want, but you can only do 3 things to complete the logic:
- Return a value: resolves the promise successfully, with that value. Note that returning nothing or undefined is the same as resolving the promise successfully with undefined
- Return another promise. The new promise for the callback will resolve or reject based on the promise you returned.
- Throw an error. This rejects the promise, with that error.
async/await Syntax for Promises
Chaining Promises can also be difficult. The newer async/await syntax lets you write Promise-handling logic with what appears to be synchronous-style syntax, including use of try/catch for handling errors.
A function must be declared using the async keyword in order to use the await keyword inside. Every async function then automatically returns a Promise with whatever value is returned. Rejected promises in an async try/catch will jump to the catch block.
// This promise chain:
function fetchStuff() {
return fetchData("/endpoint1")
.then( (result) => {
return firstProcessStep(result)
})
.then( (processedResult) => {
console.log(`Processed result: ${processedResult}`)
return processedResult;
})
.catch( (err) => {
console.error("PANIC!", err);
})
}
// Can convert to:
async function alsoFetchStuff() {
try {
let result = await fetchData("/endpoint1");
let processedResult = firstProcessStep(result);
console.log(`Processed result: ${processedResult}`)
return processedResult;
} catch (err) {
console.error("PANIC!", err);
}
}
Observables
Observables represent a progressive way of handling events, async activity, and multiple values in JavaScript. Observables are really just functions that throw values. Observables represent these streams of data. Observers represent the registered callbacks used for listening or "subscribing" to changes in these streams.
Both the Promise and Observable are used to handle async activity in JavaScript. While an Observable can do everything a Promise can, the reverse is not true.
an Observable can emit multiple values over time. A Promise only resolves once.
You can cancel an observable by unsubscribing. You can't cancel a Promise
A promise always resolves or rejects itself based on some async activity. While observables are often used with async activity (such as the Angular HTTP request client), observables can also be used for sync activities as well. For example, you can use an observable to iterate through the elements in an array.