asynchronous
First of all, javascript is synchronous after hoisting is completed. In javascript, asynchronous means executing the next code lines instead of waiting for a line to finish its job. It means that the code is not executed line by line.
console.log(1);
setTimeout(()=> console.log(2), 1000)
console.log(3);
In the code above, normally the code will be executed in order and the 1, 2, 3 would be printed. However, because the asynchronous function setTimeout, the order of the printing is 1, 3, 2.
Callback functions can also be either synchronous or asynchronous.
callback chain
Here is an example of callback chain, where first the user logs in, and if the logging in is successfull it does additional work to get the role of the user. *This code is inspired from 드림코딩 youtube channel's video
class UserStorage {
loginUser(id, password, onSuccess, onError) {
setTimeout(()=>{
if (id === 'trainer' && password === 'lee') {
onSuccess(id)
} else {
onError(new Error('not found'))
}
}, 2000)
}
getRoles(user, onSuccess, onError) {
setTimeout(()=>{
if (user === 'trainer') {
onSuccess({name: user, role: 'admin'});
} else{
onError(new Error('no access'));
}
}, 1000)
}
}
const user = new UserStorage();
user.loginUser('trainer', 'lee', (id)=>{
console.log(id);
user.getRoles(id, (object)=>{
console.log(object.name, object.role)
}, (error)=> {
console.log(error)
})
}, (error)=>{
console.log(error)
})
The code above works perfectly fine, but problems arise as more callback chaining occurs.
- The code is hard to understand. For example, if there is an error, it is hard to tell where the problem is.
- The code is hard to debug
- The code is hard to improve
This leads us to use Promise instead to handle asynchronous operations in a simpler way.
Why Promise?
When we are doing some heavy work such as getting data from the server that takes a long time, if we just run the scripts synchronously then the lines beyond the code that fetches the data won't run until the process of getting the data is completed.
Promise
Promise is a javascript object. The Promise object does various different works the user defines, and once it is done, it will return either a success or a error message. As the name Promise suggests, the code is promising that they will return a output after doing some asynchronous work.
The Promise object has two important concepts.
- state: This property shows whether the job that the Promise is doing is pending, successfull(fulfilled), or failed(rejected)
- Producer(The Promise object itself) and Consumer
const promise = new Promise((resolve, reject)=>{
// doing something heavy, such as getting data from the server.
console.log('This sentence will be printed in the console');
setTimeout(()=>{
resolve('success');
}, 2000)
})
The above is the Producer side of Promise and it takes a callback function called executor, which again takes two callback functions called resolve and reject. These functions resolve and reject are used to do the actions when the Promise is resolved(either fulfilled or rejected). One thing to note is that when the object is created from the Promise class, the contents inside the executor runs automatically without any other action.
If there is no problem, the executor function can call the resolve function and pass in a value, for example the fetched data. else, the reject(New Error('Something went wrong)) function can be called to return a error.
On the consumer side, we can chain a method called then to do something when the promise is fulfilled, or catch when the promise is rejected:
promise.then((value)=>{console.log(value)})
.catch((error)=>{console.log(error)})
Here, the variable value is the value that is passed from resolve, which is 'success' in this case.
Below is the the code that has the same functionality as the one above using callback functions, but instead uses Promise.
class UserStorage {
loginUser(id, password) {
return new Promise((resolve, reject)=>{
setTimeout(()=>{
if (id === 'trainer' && password === 'lee') {
resolve(id)
} else {
reject(new Error('not found'))
}
}, 2000)
})
}
getRoles(user) {
return new Promise((resolve, reject)=>{
setTimeout(()=>{
if (user === 'trainer') {
resolve({name: user, role: 'admin'});
} else{
reject(new Error('no access'));
}
}, 1000)
})
}
}
const user = new UserStorage();
user
.loginUser('trainer', 'lee')
.then((userid)=>{return user.getRoles(userid)})
.then((userObject)=>{console.log(userObject.name, userObject.role)})
.catch((error)=>console.log(error))
.finally(()=>console.log('everything is done'))
The catch method is called when for some reason reject function is called during the process inside the Promise there is also a finally method which also runs a callback function that will run no matter the Promise gets fulfilled or not.
async and await
async and await isn't something completely new from Promise. It is a "synthetic sugar", which means it is just a add on, and is not neccessary. async and await grammar allows Promise to seem as if they are running synchronously, but infact they are asynchronously being ran. Sometimes when using Promise, chaining it too much can also cause similar problems to when using callbacks.
function delay(time) {
if (time >= 5000) {
throw 'error';
}
return new Promise(resolve=> setTimeout(resolve), time)
}
async function doSomething() {
// do some async work...
try {
await delay(1000);
} catch(error) {
return error;
}
return 'successfull!';
}
const foo = doSomething();
foo.then((message)=>{console.log(message)});
If async
keyword is used in front of the function, then the function automatically turns into a Promise.
The await
keyword 'waits' for the Promise to get resolved, and until it is done does not continue to the next line.
when dealing with errors in async/await, we can use the try
and catch
block and for the function that is awaited(in this case the delay
function) we can throw
and error message.
Problem with await
One problem with await is that if there are more than one await
functions, they are not ran in parallel(병렬적), which means that some time is being waisted.
Take a look at the code below:
function delay(time) {
if (time >=5000) {
throw 'error';
}
return new Promise(resolve => setTimeout(resolve, time))
}
async function doFirstJob() {
await delay(2000)
return 'job 1 success.'
}
async function doSecondJob() {
await delay(2000);
return 'job 2 success.'
}
async function doSomething() {
// do some async work...
try {
const output = await doFirstJob();
const output2 = await doSecondJob();
return output + output2;
} catch(error) {
const output = error;
return output;
}
}
const foo = doSomething();
foo.then((message)=>{console.log(message)});
In the code above, there are two await function one after the other, each having a delay of 2 seconds. However, these two functions are not related.
So, it would be better to call them at the same time, but currently we cannot start to job of doSecondJob
until the first job is done. This is a waste of time.
function delay(time) {
if (time >=5000) {
throw 'error';
}
return new Promise(resolve => setTimeout(resolve, time))
}
async function doFirstJob() {
await delay(2000)
return 'job 1 success.'
}
async function doSecondJob() {
await delay(2000);
return 'job 2 success.'
}
async function doSomething() {
// do some async work...
try {
const output = Promise.all([doFirstJob(), doSecondJob()])
.then(resultsAsArray => resultsAsArray.join(' + '))
return output
} catch(error) {
const output = error;
return output;
}
}
const foo = doSomething();
foo.then((message)=>{console.log(message)});
To solve this issue, we can use the Promise.all()
method. Inside the Promise.all()
we can give a array. Then, the Promise runs all the functions inside the array in parallel.
Some operations might take longer than others. When everything is done, we can chain a .then()
and send in a callback function do something with the output.
It is important to remember that the returned value of the Promise.all()
is the array of all the results of the Promises.