Promise
The Promise object, introduced in JavaScript ES6, was first created to solve the “callback hell” problem, which arises when callback functions accumulate during asynchronous operations.
While it works on the same principle, the Promise object provides better readability, which is why it is utilized. However, using many promise variables can lead to experiencing “promise hell” as well.
Response Time of Callback Functions, Promises, and Async/Await
The response time for both callback functions and promises is almost identical. They are read sequentially in the initial hoisting environment, causing the asynchronous part to be read before the previous processes are complete.
Thus, even if there is a delay with functions like setTimeout, the response time disregards the delay and processes the next lines first.
The reason the response time for callback functions and promises is almost the same is due to slight speed differences caused by the browser’s environment.
Overall, the response time for async/await is the slowest because it waits for all asynchronous tasks (like setTimeout) to finish before proceeding in order.
Callback Functions
To understand callback functions, one must first understand “hoisting.”
Hoisting
Hoisting is the process of elevating all declarations within a function to the top of the function’s scope.
All necessary values for the function are declared at the top before the function executes.
setTimeout(callback , time)
A browser API that outputs the callback function after a specified time (in milliseconds).
Synchronous vs. Asynchronous
JavaScript is synchronous.
Synchronous callback
console.log('1')
setTimeout(() => console.log('2'), 1000)
console.log('3')
function printImmediately(print) {
print()
}
printImmediately(() => console.log('hello'))
Hoisting result:
function printImmediately(print) {
// 1. Hoisting function declaration
print()
}
console.log('1') // 2. Outputs '1'
setTimeout(() => console.log('2'), 1000) // 3. Requests to output '2' after 1 second
console.log('3') // 4. Outputs '3'
printImmediately(() => console.log('hello')) // 5. Immediately executes function, outputs 'hello'
// 6. Outputs '2' after 1 second, per browser API
Asynchronous callback
console.log('1')
setTimeout(() => console.log('2'), 1000)
console.log('3')
function printImmediately(print) {
print()
}
printImmediately(() => console.log('hello'))
function printWithDelay(print, timeout) {
setTimeout(print, timeout)
}
printWithDelay(() => console.log('async callback'), 2000)
Hoisting result:
function printImmediately(print) {
print()
}
function printWithDelay(print, timeout) {
setTimeout(print, timeout)
}
console.log('1') // Synchronous
setTimeout(() => console.log('2'), 1000) // Asynchronous ------------>
console.log('3') // Synchronous
printImmediately(() => console.log('hello')) // Synchronous
printWithDelay(() => console.log('async callback'), 2000) // Asynchronous ------------>
Promise
A Promise is “an object that can handle asynchronous states as values.”
Why are promises needed?
Promises are mainly used to display data received from servers. Generally, when implementing web applications, APIs are used to request and retrieve data from servers.
Problems with Callback Patterns
- Nested use of callback functions
function requestData1(callback) {
callback(data) // 2
}
function requestData2(callback) {
callback(data) // 4
}
function onSuccess1(data) {
console.log(data)
requestData2(onSuccess2) // 3
}
function onSuccess2(data) {
// 5
console.log(data)
}
requestData1(onSuccess1) // 1
// The flow of callback pattern code is not sequential, making it hard to read.
- Simple Promise code example
requestData1()
.then((data) => {
console.log(data)
return requestData2()
})
.then((data) => {
console.log(data)
})
- Three states of a Promise
- pending: waiting
- fulfilled: completed normally with a result
- rejected: completed abnormally.
- settled: either fulfilled or rejected state. Once a promise is settled, it cannot change to another state. It can only change from pending to fulfilled or rejected.
- How to create a Promise
const p1 = new Promise((resolve, reject) => {
resolve(data)
// or reject('error message')
})
const p2 = Promise.reject('error message')
const p3 = Promise.resolve(param)
- Promise.resolve return value
const p1 = Promise.resolve(123)
console.log(p1 !== 123) // true 이행됨 상태인 프로미스가 반환된다.
const p2 = new Promise((resolve) => setTimeout(() => resolve(10), 1))
console.log(Promise.resolve(p2) === p2) // true
// Promise.resolve 함수에 프로미스가 입력되면 그 자신이 반환된다.
- Using Promises: .then The then method is used when handling a fulfilled promise.
//requestData().then(onResolve, onReject);
// When the promise becomes fulfilled, the onResolve function is called; if rejected, the onReject function is called.
Promise.resolve(123).then((data) => console.log(data)) // 123
Promise.reject('err').then(null, (error) => console.log(error)) // err
- Chaining multiple .then calls
let requestData1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hun')
}, 1000)
})
let requestData2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('jenny')
}, 2000)
})
requestData1
.then((data) => {
console.log(data)
return requestData2 // 1
})
.then((data) => {
return data + 1 // 2
})
.then((data) => {
throw new Error('some Error') // 3
})
.then(null, (error) => {
console.log(error)
})
- When a promise is returned from a function, the then method returns that value as is.
- If a non-promise value is returned, the then method returns a fulfilled promise.
- If an exception occurs within the function, the then method returns a rejected promise.
- If it becomes a rejected state, the onReject function is called.
Promise.reject('err')
.then(() => console.log('then 1')) // 1
.then(() => console.log('then 2')) // 1
.then(
() => console.log('then 3'),
() => console.log('then 4')
) // 2
.then(
() => console.log('then 5'),
() => console.log('then 6')
) // 3
Since the promise is rejected, the first encountered onReject function is called, making the first instance omitted, and the then 4 from the second code outputs. The function that prints then 4 has undefined as its result, which creates a fulfilled promise. Therefore, then 5 is printed in the subsequent then method. The most important characteristic of the then method is that it is always called in the order it is chained.
- Using Promises 2: catch Catch is a method for handling exceptions that occur during promise execution. The catch method serves the same purpose as the onReject function of the then method. javascript 코드 복사 Promise.reject(1).then(null, (error) => { console.log(error) }) Promise.reject(1).catch((error) => { console.log(error) }) For readability, it’s better to use the catch method for exception handling than the onReject function of then.
Problems when using the onReject function of then. javascript 코드 복사 Promise.resolve().then( () => { // 1 throw new Error(‘some error’) }, (error) => { // 2 console.log(error) } ) The exception raised in the resolve function of the first then is not handled by the reject function of the same then. Executing this will result in an Unhandled promise rejection error because the rejected promise was not handled.
An example using catch instead of the onReject function.
Promise.reject(1).then(null, (error) => {
console.log(error)
})
Promise.reject(1).catch((error) => {
console.log(error)
})
For readability, it’s better to use the catch method for exception handling than the onReject function of then.
- Problems when using the onReject function of then.
Promise.resolve().then(
() => {
// 1
throw new Error('some error')
},
(error) => {
// 2
console.log(error)
}
)
The exception raised in the resolve function of the first then is not handled by the reject function of the same then. Executing this will result in an Unhandled promise rejection error because the rejected promise was not handled.
- An example using catch instead of the onReject function.
Promise.resolve()
.then(() => {
throw new Error('some error')
})
.catch((error) => {
console.log(error)
})
- Using then after catch
Promise.reject(10)
.then((data) => {
console.log('then1:', data)
return 20
})
.catch((error) => {
console.log('catch:', error)
return 30
})
.then((data) => {
console.log('then2:', data)
})
// catch: 10
// then2: 30
- Using Promises 3: Finally
- A simple code example using finally:
requestData()
.then(data => {
...
})
.catch(error => {
...
})
.finally(() => {
...
});
- The finally method does not create a new promise.
function requestData(){
return fetch()
.catch(error => {
...
})
.finally(() => {
senLogToServer('requestData finished')
})
}
requestData().then(data => console.log(data)); // 1번
In the first case, the return value of the requestData
function is the promise before the finally
method is called. Therefore, as a user of the requestData function, you don’t have to worry about the existence of the finally
method.
Using Promises Effectively
Handling in Parallel: Promise.all
Promise.all
is a function used for processing multiple promises in parallel. By chaining the then
method, you can overcome the drawback of asynchronous processes being executed sequentially.
- Asynchronous code executed sequentially:
requestData1()
.then((data) => {
console.log(data)
return requestData2()
})
.then((data) => {
console.log(data)
})
If there is no dependency between asynchronous functions, processing them in parallel is faster. By calling the asynchronous functions separately without chaining the then method, they will be processed in parallel.
- Code executed in parallel:
requestData1().then((data) => {
console.log(data)
})
requestData2().then((data) => {
console.log(data)
})
requestData1
and requestData2
run simultaneously. If you want to process multiple promises in parallel, use Promise.al
- Code using
Promise.all
:
Promise.all([requestData1(), requestData2()]).then(([data1, data2]) => {
console.log(data1, data2)
})
The Promise.all
function returns a promise. The promise returned by Promise.all
will only become fulfilled when all the input promises are fulfilled. If any promise is rejected, the promise returned by Promise.all
will also be in the rejected state.
Getting the Fastest Processed Promise: Promise.race
When any promise input to the Promise.race
function becomes fulfilled, the promise returned by Promise.race also becomes fulfilled.
- Simple code using
Promise.race
:
Promise.race([
requestData(),
new Promise((_, reject) => setTimeout(reject, 3000)),
])
.then((data) => console.log(data))
.catch((error) => console.log(error))
If the requestData
function receives data within 3 seconds, the then
method is called; otherwise, the catch
method is called.
Data Caching Using Promises
By leveraging the property of promises that maintain their state when fulfilled, you can cache data.
- Implementing caching functionality with promises:
let cachedPromise
function getData() {
cachedPromise = cachedPromise || requestData() // 1번
return cachedPromise
}
getData().then((v) => console.log(v))
getData().then((v) => console.log(v))
n the first instance, when the getData
function is called for the first time, requestData
is invoked. Once the data retrieval task is complete, the result is stored in the cachedPromise
.
Cautions When Using Promises
Don’t forget the return keyword. It’s easy to forget to input the return keyword in the internal function of the
then
method. The data of the promise object returned by thethen
method is the value returned by the internal function. If you don’t use the return keyword, the data of the promise object will be undefined.Code that forgets the return keyword:
Promise.resolve(10)
.then((data) => {
console.log(data)
Promise.resolve(20) // 2번
})
.then((data) => {
console.log(data) // 1번
})
In the first instance, undefined is printed contrary to the intention. If you input the return keyword in the second piece of code, 20 will be printed as intended.
Remember that promises are immutable objects. Promises are immutable objects.
Code written thinking that a promise can be modified:
function requestData() {
const p = Promise.resolve(10)
p.then(() => {
// 1
return 20
})
return p
}
requestData().then((v) => {
console.log(v) // 10 // 2
})
The then
method does not modify the existing object; it returns a new promise. In the second case, if you want 20 to be printed, you need to modify the requestData
function as follows.
- Code returning the promise created by the
then
method:
function requestData() {
return Promise.resolve(10).then((v) => {
return 20
})
}
Avoid using nested promises. Using nested promises can lead to “promise hell” similar to callback hell.
requestData1().then(result1 => { requestData2(result2 => { .... }); });
This is not readable, so let’s refactor it as in the following code.
- Refactored code to avoid nesting:
requestData1()
.then(result1 => {
return requestData2(result1)
})
.then(result2 => {
return ... // 1번
})
If you need to reference the result1 variable in the first instance, how can you do that?
You can solve the variable reference issue without nesting by using
Promise.all
.Code solving variable reference problem using
Promise.all
:
requestData1()
.then(result1 => {
return Promise.all([result1, requestData2(result1)]) // 1
.then(([result1, result2]) => {
...........
});
In the first instance, if you input non-promise values into the array for the Promise.all
function, those values will be treated as if they are fulfilled promises.
Pay Attention to Exception Handling in Synchronous Code
When using promises like synchronous (sync) code, you should pay attention to exception handling.
- Code where exceptions occurring in synchronous code are not handled:
function requestData() {
doSync() // 1
return fetch()
.then((data) => console.log(data))
.catch((error) => console.log(error))
}
In the first instance, if the doSync
function does not necessarily need to be called before fetch, it’s better to put it inside the then
method like this:
- Code where synchronous code also handles exceptions:
function requestData() {
return fetch()
.then((data) => {
doSync()
console.log(data)
})
.catch((error) => console.log(Error))
}
Exceptions occurring in doSync
will be handled by the catch
method.
Enhanced Asynchronous Programming: async, await
Understanding async/await:
While promises exist as objects, async/await is a concept applied to functions.
A function returning a promise using async/await:
async function getData() {
return 123 //Promise {<fulfilled>: 123}
}
getData().then((data) => console.log(data)) // 123 Promise {<fulfilled>: undefined}
- A function returning a promise using async/await:
async function getData() {
return Promise.resolve(123)
}
getData().then((data) => console.log(data))
Similar to the then
method of promises, if the value returned within the async/await function is a promise, that object will be returned as is.
- Cases where exceptions occur in async/await functions:
async function getData() {
throw new Error('123')
}
getData().catch((error) => console.log(error))
// Error : 123
- Using the
await
keyword:
function requestData(value) {
return new Promise((resolve) =>
setTimeout(() => {
console.log('requestData:', value)
resolve(value)
}, 100)
)
}
async function getData() {
const data1 = await requestData(10) //1
const data2 = await requestData(20) //1
console.log(data1, data2) // 2
return [data1, data2]
}
getData()
// requestData: 10 // 3
// requestData: 20 // 3
// 10, 20 // 3
In the first instance, the code in the second instance will not execute until the promise returned by requestData
is fulfilled. Therefore, the result of calling the getData
function is reflected in the third instance.
The async
keyword can only be used within async/await functions. Using it in a regular function will cause an error.
await
키워드는 async 키워드 없이 사용 할 수 없다.
function getData(){
const data = await requestData(10); // Error
console.log(data);
}
async/await is more readable than promises.
- Comparing async/await and Promises
function getDataPromise() {
asyncFunc1()
.then((data) => {
console.log(data)
return asyncFunc2()
})
.then((data) => {
console.log(data)
})
} // 1번 프로미스로 작성한 코드
async function getDataAsync() {
const data1 = await asyncFunc1()
console.log(data1)
const data2 = await asyncFunc2()
console.log(data2)
} // 2번 async await로 작성한 코드
The code written using async/await has better readability. The reason for its conciseness is that async/await functions do not require calling the then
method.
- Comparing Readability in Highly Dependent Code
function getDataPromise() {
return asyncFunc1()
.then(data1 => Promise.all([data1, asyncFunc2(data1)])) // 1
.then([data1, data2]) => {
return asyncFunc3(data1,data2);
});
}
async function getDataAsync() { // 2
const1 data1 = await asyncFunc1();
const2 data2 = await asyncFunc2(data1);
return asyncFunc3(data1, data2);
}
In 1, Promise.all
is used to pass two return values to the asyncFunc3
function. In 2, the async/await function remains intuitive even with complex dependencies.
Using async/await Effectively
- Running Asynchronous Functions in Parallel
- Sequentially running asynchronous code
async function getData() {
const data1 = await asyncFunc1()
const data2 = await asyncFunc2()
// ...
}
If there are no dependencies between the two functions, it is better to run them simultaneously. Promises execute asynchronously as soon as they are created. Therefore, by creating two promises first and using the await
keyword later, we can achieve parallel execution.
- Running Asynchronous Code in Parallel by Using
await
Later
async function getData() {
const p1 = asyncFunc1()
const p2 = asyncFunc2()
const data1 = await p1
const data2 = await p2
}
- Using
Promise.all
to Process in Parallel
async function getData() {
const [data1, data2] = await Promise.all([asyncFunc1(), asyncFunc2()])
// ....
}
- Handling Exceptions It is advisable to handle exceptions occurring within async/await functions using try/catch blocks. Both synchronous and asynchronous functions can be handled in the catch block.
async function getData() {
try {
await doAsync()
return doSync()
} catch (error) {
console.log(error)
}
}
All exceptions that occur in asynchronous and synchronous functions are handled in the catch block.
- Support for Thenables in async/await
Thenables are objects that behave like promises.
- Example of Using Thenables in async/await Functions
class ThenableExample {
then(resolve, reject) {
// 1번
setTimeout(() => resolve(123), 1000)
}
}
async function asyncFunc() {
const result = await new ThenableExaple() // 2번
console.log(result) // 123
}
In 1, the ThenableExample
class has a then
method, so any object created from the ThenableExample
class is considered a Thenable. In 2, the async/await function treats Thenables like promises.
- Difference Between sync/await and Promises Promise: Ignores the execution of the code when it is encountered and continues executing the next code. async/await: When it encounters await, it waits until the code is finished (until the request is completed, until it is not pending) before executing the next code.