function loadScript(src) {
// creates a <script> tag and append it to the page
// this causes the script with given src to start loading and run when complete
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
loadScript('/my/script.js');
// the code below loadScript
// doesn't wait for the script loading to finish
// ...
Let’s say we need to use the new script as soon as it loads. It declares new functions, and we want to run them.
loadScript('/my/script.js'); // the script has "function newFunction() {…}"
newFunction(); // no such function!
Let’s add a callback function as a second argument to loadScript that should execute when the script loads:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
Now if we want to load multiple scripts synchronously, we can use loadscript
in this fashion.
loadscript('script1.js', function(){
loadscript('script2.js', function(){
loadscript('script3,js', function(){
})
})
})
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onerror = callback(new Error('failed to load script'))
script.onload = () => callback(null, script);
document.head.append(script);
}
loadScript('errorscript',function(error, script){
if(error){
console.log('error')
} else {
//success
}
})
It’s called the “error-first callback” style.
The convention is:
- The first argument of the callback is reserved for an error if it occurs. Then
callback(err)
is called. - The second argument (and the next ones if needed) are for the successful result. Then
callback(null, result1, result2…)
is called.
At first glance, it looks like a viable approach to asynchronous coding. And indeed it is. For one or maybe two nested calls it looks fine.
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
});
}
});
}
});
In the code above:
- We load 1.js, then if there’s no error…
- We load 2.js, then if there’s no error…
- We load 3.js, then if there’s no error – do something else (*).
As calls become more nested, the code becomes deeper and increasingly more difficult to manage, especially if we have real code instead of ...
that may include more loops, conditional statements and so on.
That’s sometimes called “callback hell” or “pyramid of doom.”
The “pyramid” of nested calls grows to the right with every asynchronous action. Soon it spirals out of control.
So this way of coding isn’t very good.
We can try to alleviate the problem by making every action a standalone function, like this:
loadScript('1.js', step1);
function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}
function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}
function step3(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
}
It works, but the code looks like a torn apart spreadsheet. It’s difficult to read, and you probably noticed that one needs to eye-jump between pieces while reading it. That’s inconvenient, especially if the reader is not familiar with the code and doesn’t know where to eye-jump.
Also, the functions named step*
are all of single use, they are created only to avoid the “pyramid of doom.” No one is going to reuse them outside of the action chain. So there’s a bit of namespace cluttering here.
A “producing code” that does something and takes time. For instance, some code that loads the data over a network. That’s a “singer”.
A “consuming code” that wants the result of the “producing code” once it’s ready. Many functions may need that result. These are the “fans”.
A promise is a special JavaScript object that links the “producing code” and the “consuming code” together. In terms of our analogy: this is the “subscription list”. The “producing code” takes whatever time it needs to produce the promised result, and the “promise” makes that result available to all of the subscribed code when it’s ready.
let promise = new Promise(function(resolve, reject) {
// executor (the producing code, "singer")
});
The function passed to new Promise is called the executor. When new Promise is created, the executor runs automatically. It contains the producing code which should eventually produce the result.
When the executor obtains the result, be it soon or late, doesn’t matter, it should call one of these callbacks:
resolve(value)
— if the job is finished successfully, with resultvalue
.reject(error)
— if an error has occurred,error
is the error object.
The promise object returned by the new Promise constructor has these internal properties:
state
— initially "pending", then changes to either "fulfilled" when resolve is called or "rejected" when reject is called.result
— initially undefined, then changes to value when resolve(value) called or error when reject(error) is called.
let p = new Promise(function(resolve, reject){
setTimeout(() => {
resolve('done)
},1000)
})
We can see two things by running the code above:
-
The executor is called automatically and immediately (by new Promise).
-
The executor receives two arguments: resolve and reject. These functions are pre-defined by the JavaScript engine, so we don’t need to create them. We should only call one of them when ready.
After one second of “processing” the executor calls resolve("done") to produce the result. This changes the state of the promise object:
Now with error
let p = new Promise(function(resolve, reject){
setTimeout(() => {
reject(new Error('Oops!'))
},1000)
})
A Promise object serves as a link between the executor (the “producing code” or “singer”) and the consuming functions (the “fans”), which will receive the result or error. Consuming functions can be registered (subscribed) using methods .then, .catch and .finally.
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second
The call .catch(f) is a complete analog of .then(null, f), it’s just a shorthand.
The call .finally(f)
is similar to .then(f, f)
in the sense that f
always runs when the promise is settled: be it resolve or reject.
finally
is a good handler for performing cleanup, e.g. stopping our loading indicators, as they are not needed anymore, no matter what the outcome is.
new Promise((resolve, reject) => {
/* do something that takes time, and then call resolve/reject */
})
// runs when the promise is settled, doesn't matter successfully or not
.finally(() => stop loading indicator)
// so the loading indicator is always stopped before we process the result/error
.then(result => show result, err => show error)
But finally(f)
isn’t exactly an alias of then(f,f)
though. There are few subtle differences:
-
A finally handler has no arguments. In finally we don’t know whether the promise is successful or not. That’s all right, as our task is usually to perform “general” finalizing procedures.
-
A finally handler passes through results and errors to the next handler.
For eg:
new Promise((resolve, reject) => {
setTimeout(() => resolve("result"), 2000)
})
.finally(() => alert("Promise ready"))
.then(result => alert(result)); // <-- .then handles the result
Similarly for error
new Promise((resolve, reject) => {
throw new Error("error");
})
.finally(() => alert("Promise ready"))
.catch(err => alert(err)); // <-- .catch handles the error object
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
Usage:
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));
Differences between promises
and callback
Promises | Callback |
---|---|
Promises allow us to do things in the natural order. First, we run loadScript(script) , and .then we write what to do with the result. |
We must have a callback function at our disposal when calling loadScript(script, callback) . In other words, we must know what to do with the result before loadScript is called. |
We can call .then on a Promise as many times as we want. Each time, we’re adding a new “fan”, a new subscribing function, to the “subscription list” |
There can be only one callback. |