Managing Callbacks / Promises CSCI-UA.0467-002
Initially, dealing with asynchronous tasks is a bit tricky. A lot of times, you want to run code only after a task completes.
If you have a task, and you don't know it will finish, how do you ensure that it's done before running other code that's dependent on it? →
we've seen a common pattern for tasks that are asynchronous; they expect a callback
the callback is fired when the task is finished
so we put any code that's dependent on that task within the callback
Let's start off by creating a function that will take some amount of time to produce an output →
name the function delayedShout
it should accept one argument, s
, the String
that it will print out
delayedShout
will print out s
as uppercase with exclamation points after ~1 to ~3 seconds
example: delayedShout('foo')
will print out FOO!!!
around 1 to 3 seconds after the initial call
function delayedShout(s) {
// ~1.5 to ~3.5 seconds
const delay = 1000 + Math.random() * 2000;
setTimeout(
() => {
const shout = `${s.toUpperCase()}!!!`
console.log(shout);
},
delay
);
}
delayedShout
Print?Based on our implementation of delayedShout
, what will the following code output? →
console.log('before');
delayedShout('foo');
console.log('after');
before
after
FOO!!!
delayedShout
How can we change our implementation of delayedShout
so that we are able to do something (like print 'after') when delayedShout
finishes processing and outputting the string? →
add a callback as a parameter
call the callback after processing…
function delayedShout(s, cb) {
// ~1.5 to ~3.5 seconds
const delay = 1000 + Math.random() * 2000;
setTimeout(
() => {
const shout = `${s.toUpperCase()}!!!`
console.log(shout);
cb();
},
delay
);
}
delayedShout
How do we call our new delayedShout
so that:
the string "after"
is printed …
when delayedShout
finishes processing and outputting the original string
for example:
before
FOO!!!
after
console.log('before');
delayedShout('foo', () => console.log('after'));
This version waits some time before throwing an error due to length &arr;
function delayedShout(s, cb) {
// ~1.5 to ~3.5 seconds
const delay = 1000 + Math.random() * 2000;
setTimeout(
() => {
if(s.length == 0) {
throw 'empty string';
}
const shout = `${s.toUpperCase()}!!!`
console.log(shout);
cb();
},
delay
);
}
Based on our new implementation of delayedShout
, what will happen if we run the code below? →
Will this print out something, nothing, or an error? If it prints out something, what will it print out?
console.log('before');
try {
delayedShout('', () => console.log('after'));
} catch(e) {
console.log(e);
}
We get an error despite the try catch:
as the callback (which causes the error) gets called later
(after the try catch block has been executed)
but the call to delayedShout
finishes running without an error.
So, how do we implement exception handling? We have a couple of choices, but both involve a callback →
call the original callback with error as the first parameter
add another callback as a parameter to handle errors
We've seen something like this before, right? For example fs.readFile
has a very similar API!
So, an alternate way to deal with this is to use an API that allows us to code as if we were dealing with simple, sequential operations.
One of these APIs, Promise, is in ES6 and is available on most browsers.
A Promise is an object that represents an asynchronous action - some operation that may or may not have been completed yet.
For example, a Promise may represent:
retrieving data from a database can be a promise
writing to a file
making an http request
Again, a Promise is an object that represents an async task →
Consequently, a Promise can be in one of the following states:
pending - the task hasn't been completed yet (still getting a url, reading a file, etc.)
fulfilled - the task has completed successfully
rejected - the task did not complete successfully (error state)
Another way to think of a promise is:
a value that can be returned
that eventually becomes the return the result or a thrown error from an async task
To create a Promise
use the Promise
constructor: →
it has one parameter, a function called the executor
the executor function is going to do some async stuff
it is executed immediately, even before the constructor returns!
the executor has two parameters:
a function to call if the task succeeded (fulfill
)
a function to call if the task failed (reject
)
both of these functions have a single argument
const p = new Promise(function(fulfill, reject) {
// do something async
if(asyncTaskCompletedSuccessfully) {
fulfill('Success!');
} else {
reject('Failure!');
}
});
Promise objects have a couple of methods that allow the fulfill
and reject
arguments of the executor
function to be set: →
then(fulfill, reject)
- sets both the fulfill
and reject
functions
catch(reject)
- only sets the reject
function
then
can represent the next step to execute when a Promise
completes (either successfully or fails). →
it accepts a couple of callbacks as parameters…
the thing to do if our Promise was resolved or successful
the thing to do if our Promise was rejected or unsuccessful
these callbacks have a single parameter
the value that was passed into the original succeed or reject function call in the original Promise
think of these as the succeed and fail in the original promise
Let's take a look at how then
works:
start with a Promise that immediately is fulfilled
(it's as if the async task finished instantly)
What is the output of this code, if any? →
const p = new Promise(function(fulfill, reject) {
fulfill('Success!');
});
p.then(function(val) {
console.log(val);
})
Success!
Let's take a closer look at what's happening here: →
const p = new Promise(function(fulfill, reject) {
fulfill('Success!');
});
p.then(function(val) {
console.log(val);
})
The first argument to then
is a function that takes a single argument and logs out that argument
Using then
sets fulfill
to the function above, so calling fulfill
results in logging out the value
In fact any function that takes a single argument would work as the first argument to then
This would result in the same output:
p.then(console.log);
The functions passed to then
are guaranteed to be executed AFTER the Promise is created. →
This is true even if it looks like fulfill
is called immediately and before then
is called!. What's the output of this code? →
const p1 = new Promise(function(fulfill, reject) {
console.log('begin');
fulfill('succeeded');
console.log('end');
});
p1.then(console.log);
begin
end
succeeded
// the fulfill function, console.log, is
// guaranteed to be called after the Promise
// is created even though it looks like fulfill
// is called between logging begin and end!
then
's Second ArgumentTo specify what happens when a Promise results in an error or if the async task fails, use then
's 2nd argument. →
const p = new Promise(function(fulfill, reject) {
reject('did not work!');
});
p.then(console.log, function(val) {
console.log('ERROR', val);
});
The code above results in the following output …
ERROR did not work!
catch
You can also use the method catch
to specify the reject
function. →
const p = new Promise(function(fulfill, reject) {
reject('did not work!');
});
p.catch(function(val) {
console.log('ERROR', val);
});
then
always returns a Promise →
if the fulfill
function returns a Promise
, then
will return that Promise
if the fulfill
function returns a value, then
will return a Promise
that immediately fulfills with the return value
That sounds convoluted… Let's see some examples. →
then
return ValueStarting with a Promise
…
const p1 = new Promise(function(fulfill, reject) {
fulfill(1);
});
The fulfill
function passed to then
returns a Promise
, so then
returns that same Promise
object (which is assigned to p2
)
const p2 = p1.then(function(val) {
console.log(val);
return new Promise(function(fulfill, reject) {
fulfill(val + 1);
});
});
Because p2
is another Promise
, we can call then
on that too.
p2.then(console.log);
So the resulting output is… →
1
2
Let's make a minor modification to the code in the previous slide. Again, start with a Promise… →
const p1 = new Promise(function(fulfill, reject) {
fulfill(1);
});
This time, though, instead of fulfill
returning a Promise
, it'll return a regular value.
const p2 = p1.then(function(val) {
console.log(val);
return val + 1;
});
Again, let's try calling then
on p2
(but is p2
a Promise
… or will an error occur!?)
p2.then(console.log);
p2
is still a Promise
If fulfill
returns a non-Promise, then
will return a Promise
that immediately calls fulfill with the value that was returned. →
Consequently, the following two code samples return the same Promise
for p2
:
const p2 = p1.then(function(val) {
console.log(val);
return new Promise(function(fulfill, reject) {
fulfill(val + 1);
});
});
const p2 = p1.then(function(val) {
console.log(val);
return val + 1;
});
Assuming we have a function called get that retrieves a url… we tend to want to do this →
const data = get(url);
parseResult(data);
But if our get is asynchronous, we can't guarantee that get finishes before parseResult is called (so callback functions it is) →
get(url, function(data) {
parseResult(data);
});
Ok. We get asynchronous tasks… and we understand that:
if code depends on an async task
put in async task's callback
and it'll get executed when the task is finished
So… what happens if we have async tasks that are dependent on other async tasks? For example:
retrieving a url results in a second url
which also has to be retrieved…
and maybe, in turn, the second url produces a third!
Let's assume that we have our get function:
it takes two arguments, a url and a callback
and the callback has a single parameter, the response data from the request
Using our imaginary get function, what would this look like? →
A tiny pyramid. ▲ ▲ ▲ ▲ ▲
We use a bunch of nested callbacks… (the pyramid is the white space to the left).
get(url, function(data) {
const urlTwo = parseResult(data);
get(urlTwo, function(data) {
const urlThree = parseResult(data);
get(urlThree, function(data) {
console.log("Aaaand we're done");
});
});
});
Create 3 json files that each have an object with a url
property holding the url of another json file. Then retrieve these files one by one… →
Create an express app to serve up our files…
Create a bunch of json files in a directory called data
within public
tango.json
: { "url":"http://localhost:3000/data/uniform.json" }
uniform.json
: { "url":"http://localhost:3000/data/victor.json" }
victor.json
: {}
Create a page that uses external JavaScript that…
Uses XMLHttpRequest to pull retrieve tango.json
Extract the url, and retrieve it… and do the same for the third url…
Oh hello scrollbars. This won't even fit on this slide.
const url = 'http://localhost:3000/data/tango.json';
req1 = new XMLHttpRequest();
req1.open('GET', url, true);
req1.addEventListener('load', function() {
console.log('loading req1');
if(req1.status >= 200 && req1.status < 400) {
console.log(req1.responseText);
const data1 = JSON.parse(req1.responseText)
console.log(data1.url);
req2 = new XMLHttpRequest();
req2.open('GET', data1.url, true);
req2.addEventListener('load', function() {
console.log('loading req2');
if(req2.status >= 200 && req2.status < 400) {
console.log(req2.responseText);
const data2 = JSON.parse(req2.responseText)
console.log(data2.url);
req3 = new XMLHttpRequest();
req3.open('GET', data2.url, true);
req3.addEventListener('load', function() {
console.log('loading req3');
if(req3.status >= 200 && req3.status < 400) {
console.log(req3.responseText);
console.log('done');
}
});
req3.send();
}
});
req2.send();
}
});
req1.send();
Oof. Apologies for making your eyes bleed.
So much nesting.
Such repetition!
What can we do to tame this a bit? →
hey… maybe stop using so many anonymous functions (start naming those things)
and/or wrap up URL retrieval and data extraction into separate functions
get(url, cb)
extractURL(json)
So… this function will retrieve a url, and when it gets a response, it'll call the callback with the response text.
function get(url, cb) {
console.log('getting ', url);
req = new XMLHttpRequest();
req.open('GET', url, true);
req.addEventListener('load', function() {
console.log('loading req');
if(req.status >= 200 && req.status < 400) {
console.log(req.responseText);
cb(req.responseText);
}
});
req.send();
}
This one's simple
function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}
Ah. Much nicer.
const url = 'http://localhost:3000/data/tango.json';
get(url, function(responseText) {
const url2 = extractURL(responseText);
get(url2, function(responseText) {
const url3 = extractURL(responseText);
get(url3, function(responseText) {
console.log('done');
});
});
});
We still get a tiny pyramid, though. To get around that, we can:
for this example only (since it's doing the same thing for each), encapsulate each level of nesting in a single function
use Promises
Getting and extracting were repeated 3 times. Why don't we just wrap this in another function? →
(this only works because we're doing the same exact thing in each level of callback nesting).
function getAndExtract(url) {
get(url, function(responseText) {
const url = extractURL(responseText);
if(url) {
getAndExtract(url);
} else {
console.log('done');
}
});
}
getAndExtract(url);
So maybe our version of get
will now just give back a Promise to wrap the async code. →
function get(url) {
return new Promise(function(fulfill, reject) {
console.log('getting ', url);
req = new XMLHttpRequest();
req.open('GET', url, true);
req.addEventListener('load', function() {
if(req.status >= 200 && req.status < 400) {
fulfill(req.responseText);
} else {
reject('got bad status code ' + req.status);
}
});
// also reject for error event listener!
req.send();
});
}
function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}
const url = 'http://localhost:3000/data/tango.json';
get(url)
.then(extractURL)
.then(get)
.then(extractURL)
.then(get)
.then(extractURL)
.then(function(val){
console.log(val);
console.log('done');
});
So, promises are kind of complicated, but in the end, they do simplify things. Some things that we didn't cover that further show the power of using the Promise API are:
error handling
having code trigger only when multiple async tasks are finished (rather than just one)
Using Promise
s seemed to complicate AJAX rather than make it easier.
It's certainly tricky manually wrapping async tasks with Promises, but:
fortunately for us, we'll mostly encounter Promises as the result of using some built-in functions in JavaScript, like fetch →
the fetch
api provides a global function fetch
that allows the retrieval of a url
but wraps the result of that retrieval in a Promise:
fetch(url)
.then(function(response) { return response.text(); })
.then(handleResponse)
The Fetch API offers a Request
and Response
object, as well as a global fetch
function →
The fetch
function:
takes one argument, a url
it returns a promise
the promise is resolved with Response
object
fetch('http://foo.bar.baz/qux.json')
.then(function(response) {
# Response object!
return response.json();
})
.then(function(data) {
console.log(JSON.stringify(data));
});
async
and await
An alternative to using then
is to use async
and await
await
will wait for the Promise
on the right to resolve… and the await
expression will be evaluated to the value that the Promise
is resolved with
await
can only be used at the top level of your code outside of a function
… or within a function declared as async
it allows async operations to run sequentially
async
declared functions are functions that are explicitly marked as asynchronous
they return a promise
they allow await
to be used
.then
to async
and await
As an example of using async and await, we have this code:
fetch('http://foo.bar.baz/qux.json')
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log(JSON.stringify(data));
});
Which can be converted to:
const response = await fetch('http://foo.bar.baz/qux.json');
const data = await response.json();
console.log(data);
(note that the await
code above must be in an async
declared function or at the top-level)
The Response
object has a few methods and properties →
status
headers
json()
- response body parsed json
text()
- response body
A fetch
call can be configured by passing in an object as the second argument →
// example of options for a POST
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: 'foo=bar'
}
Check out mdn's Using Fetch for more detailed examples.
Initially, dealing with asynchronous tasks is a bit tricky. A lot of times, you want to run code only after a task completes.
If you have a task, and you don't know it will finish, how do you ensure that it's done before running other code that's dependent on it? →
we've seen a common pattern for tasks that are asynchronous; they expect a callback
the callback is fired when the task is finished
so we put any code that's dependent on that task within the callback
Let's start off by creating a function that will take some amount of time to produce an output →
name the function delayedShout
it should accept one argument, s
, the String
that it will print out
delayedShout
will print out s
as uppercase with exclamation points after ~1 to ~3 seconds
example: delayedShout('foo')
will print out FOO!!!
around 1 to 3 seconds after the initial call
function delayedShout(s) {
// ~1.5 to ~3.5 seconds
const delay = 1000 + Math.random() * 2000;
setTimeout(
() => {
const shout = `${s.toUpperCase()}!!!`
console.log(shout);
},
delay
);
}
delayedShout
Print?Based on our implementation of delayedShout
, what will the following code output? →
console.log('before');
delayedShout('foo');
console.log('after');
before
after
FOO!!!
delayedShout
How can we change our implementation of delayedShout
so that we are able to do something (like print 'after') when delayedShout
finishes processing and outputting the string? →
add a callback as a parameter
call the callback after processing…
function delayedShout(s, cb) {
// ~1.5 to ~3.5 seconds
const delay = 1000 + Math.random() * 2000;
setTimeout(
() => {
const shout = `${s.toUpperCase()}!!!`
console.log(shout);
cb();
},
delay
);
}
delayedShout
How do we call our new delayedShout
so that:
the string "after"
is printed …
when delayedShout
finishes processing and outputting the original string
for example:
before
FOO!!!
after
console.log('before');
delayedShout('foo', () => console.log('after'));
This version waits some time before throwing an error due to length &arr;
function delayedShout(s, cb) {
// ~1.5 to ~3.5 seconds
const delay = 1000 + Math.random() * 2000;
setTimeout(
() => {
if(s.length == 0) {
throw 'empty string';
}
const shout = `${s.toUpperCase()}!!!`
console.log(shout);
cb();
},
delay
);
}
Based on our new implementation of delayedShout
, what will happen if we run the code below? →
Will this print out something, nothing, or an error? If it prints out something, what will it print out?
console.log('before');
try {
delayedShout('', () => console.log('after'));
} catch(e) {
console.log(e);
}
We get an error despite the try catch:
as the callback (which causes the error) gets called later
(after the try catch block has been executed)
but the call to delayedShout
finishes running without an error.
So, how do we implement exception handling? We have a couple of choices, but both involve a callback →
call the original callback with error as the first parameter
add another callback as a parameter to handle errors
We've seen something like this before, right? For example fs.readFile
has a very similar API!
So, an alternate way to deal with this is to use an API that allows us to code as if we were dealing with simple, sequential operations.
One of these APIs, Promise, is in ES6 and is available on most browsers.
A Promise is an object that represents an asynchronous action - some operation that may or may not have been completed yet.
For example, a Promise may represent:
retrieving data from a database can be a promise
writing to a file
making an http request
Again, a Promise is an object that represents an async task →
Consequently, a Promise can be in one of the following states:
pending - the task hasn't been completed yet (still getting a url, reading a file, etc.)
fulfilled - the task has completed successfully
rejected - the task did not complete successfully (error state)
Another way to think of a promise is:
a value that can be returned
that eventually becomes the return the result or a thrown error from an async task
To create a Promise
use the Promise
constructor: →
it has one parameter, a function called the executor
the executor function is going to do some async stuff
it is executed immediately, even before the constructor returns!
the executor has two parameters:
a function to call if the task succeeded (fulfill
)
a function to call if the task failed (reject
)
both of these functions have a single argument
const p = new Promise(function(fulfill, reject) {
// do something async
if(asyncTaskCompletedSuccessfully) {
fulfill('Success!');
} else {
reject('Failure!');
}
});
Promise objects have a couple of methods that allow the fulfill
and reject
arguments of the executor
function to be set: →
then(fulfill, reject)
- sets both the fulfill
and reject
functions
catch(reject)
- only sets the reject
function
then
can represent the next step to execute when a Promise
completes (either successfully or fails). →
it accepts a couple of callbacks as parameters…
the thing to do if our Promise was resolved or successful
the thing to do if our Promise was rejected or unsuccessful
these callbacks have a single parameter
the value that was passed into the original succeed or reject function call in the original Promise
think of these as the succeed and fail in the original promise
Let's take a look at how then
works:
start with a Promise that immediately is fulfilled
(it's as if the async task finished instantly)
What is the output of this code, if any? →
const p = new Promise(function(fulfill, reject) {
fulfill('Success!');
});
p.then(function(val) {
console.log(val);
})
Success!
Let's take a closer look at what's happening here: →
const p = new Promise(function(fulfill, reject) {
fulfill('Success!');
});
p.then(function(val) {
console.log(val);
})
The first argument to then
is a function that takes a single argument and logs out that argument
Using then
sets fulfill
to the function above, so calling fulfill
results in logging out the value
In fact any function that takes a single argument would work as the first argument to then
This would result in the same output:
p.then(console.log);
The functions passed to then
are guaranteed to be executed AFTER the Promise is created. →
This is true even if it looks like fulfill
is called immediately and before then
is called!. What's the output of this code? →
const p1 = new Promise(function(fulfill, reject) {
console.log('begin');
fulfill('succeeded');
console.log('end');
});
p1.then(console.log);
begin
end
succeeded
// the fulfill function, console.log, is
// guaranteed to be called after the Promise
// is created even though it looks like fulfill
// is called between logging begin and end!
then
's Second ArgumentTo specify what happens when a Promise results in an error or if the async task fails, use then
's 2nd argument. →
const p = new Promise(function(fulfill, reject) {
reject('did not work!');
});
p.then(console.log, function(val) {
console.log('ERROR', val);
});
The code above results in the following output …
ERROR did not work!
catch
You can also use the method catch
to specify the reject
function. →
const p = new Promise(function(fulfill, reject) {
reject('did not work!');
});
p.catch(function(val) {
console.log('ERROR', val);
});
then
always returns a Promise →
if the fulfill
function returns a Promise
, then
will return that Promise
if the fulfill
function returns a value, then
will return a Promise
that immediately fulfills with the return value
That sounds convoluted… Let's see some examples. →
then
return ValueStarting with a Promise
…
const p1 = new Promise(function(fulfill, reject) {
fulfill(1);
});
The fulfill
function passed to then
returns a Promise
, so then
returns that same Promise
object (which is assigned to p2
)
const p2 = p1.then(function(val) {
console.log(val);
return new Promise(function(fulfill, reject) {
fulfill(val + 1);
});
});
Because p2
is another Promise
, we can call then
on that too.
p2.then(console.log);
So the resulting output is… →
1
2
Let's make a minor modification to the code in the previous slide. Again, start with a Promise… →
const p1 = new Promise(function(fulfill, reject) {
fulfill(1);
});
This time, though, instead of fulfill
returning a Promise
, it'll return a regular value.
const p2 = p1.then(function(val) {
console.log(val);
return val + 1;
});
Again, let's try calling then
on p2
(but is p2
a Promise
… or will an error occur!?)
p2.then(console.log);
p2
is still a Promise
If fulfill
returns a non-Promise, then
will return a Promise
that immediately calls fulfill with the value that was returned. →
Consequently, the following two code samples return the same Promise
for p2
:
const p2 = p1.then(function(val) {
console.log(val);
return new Promise(function(fulfill, reject) {
fulfill(val + 1);
});
});
const p2 = p1.then(function(val) {
console.log(val);
return val + 1;
});
Assuming we have a function called get that retrieves a url… we tend to want to do this →
const data = get(url);
parseResult(data);
But if our get is asynchronous, we can't guarantee that get finishes before parseResult is called (so callback functions it is) →
get(url, function(data) {
parseResult(data);
});
Ok. We get asynchronous tasks… and we understand that:
if code depends on an async task
put in async task's callback
and it'll get executed when the task is finished
So… what happens if we have async tasks that are dependent on other async tasks? For example:
retrieving a url results in a second url
which also has to be retrieved…
and maybe, in turn, the second url produces a third!
Let's assume that we have our get function:
it takes two arguments, a url and a callback
and the callback has a single parameter, the response data from the request
Using our imaginary get function, what would this look like? →
A tiny pyramid. ▲ ▲ ▲ ▲ ▲
We use a bunch of nested callbacks… (the pyramid is the white space to the left).
get(url, function(data) {
const urlTwo = parseResult(data);
get(urlTwo, function(data) {
const urlThree = parseResult(data);
get(urlThree, function(data) {
console.log("Aaaand we're done");
});
});
});
Create 3 json files that each have an object with a url
property holding the url of another json file. Then retrieve these files one by one… →
Create an express app to serve up our files…
Create a bunch of json files in a directory called data
within public
tango.json
: { "url":"http://localhost:3000/data/uniform.json" }
uniform.json
: { "url":"http://localhost:3000/data/victor.json" }
victor.json
: {}
Create a page that uses external JavaScript that…
Uses XMLHttpRequest to pull retrieve tango.json
Extract the url, and retrieve it… and do the same for the third url…
Oh hello scrollbars. This won't even fit on this slide.
const url = 'http://localhost:3000/data/tango.json';
req1 = new XMLHttpRequest();
req1.open('GET', url, true);
req1.addEventListener('load', function() {
console.log('loading req1');
if(req1.status >= 200 && req1.status < 400) {
console.log(req1.responseText);
const data1 = JSON.parse(req1.responseText)
console.log(data1.url);
req2 = new XMLHttpRequest();
req2.open('GET', data1.url, true);
req2.addEventListener('load', function() {
console.log('loading req2');
if(req2.status >= 200 && req2.status < 400) {
console.log(req2.responseText);
const data2 = JSON.parse(req2.responseText)
console.log(data2.url);
req3 = new XMLHttpRequest();
req3.open('GET', data2.url, true);
req3.addEventListener('load', function() {
console.log('loading req3');
if(req3.status >= 200 && req3.status < 400) {
console.log(req3.responseText);
console.log('done');
}
});
req3.send();
}
});
req2.send();
}
});
req1.send();
Oof. Apologies for making your eyes bleed.
So much nesting.
Such repetition!
What can we do to tame this a bit? →
hey… maybe stop using so many anonymous functions (start naming those things)
and/or wrap up URL retrieval and data extraction into separate functions
get(url, cb)
extractURL(json)
So… this function will retrieve a url, and when it gets a response, it'll call the callback with the response text.
function get(url, cb) {
console.log('getting ', url);
req = new XMLHttpRequest();
req.open('GET', url, true);
req.addEventListener('load', function() {
console.log('loading req');
if(req.status >= 200 && req.status < 400) {
console.log(req.responseText);
cb(req.responseText);
}
});
req.send();
}
This one's simple
function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}
Ah. Much nicer.
const url = 'http://localhost:3000/data/tango.json';
get(url, function(responseText) {
const url2 = extractURL(responseText);
get(url2, function(responseText) {
const url3 = extractURL(responseText);
get(url3, function(responseText) {
console.log('done');
});
});
});
We still get a tiny pyramid, though. To get around that, we can:
for this example only (since it's doing the same thing for each), encapsulate each level of nesting in a single function
use Promises
Getting and extracting were repeated 3 times. Why don't we just wrap this in another function? →
(this only works because we're doing the same exact thing in each level of callback nesting).
function getAndExtract(url) {
get(url, function(responseText) {
const url = extractURL(responseText);
if(url) {
getAndExtract(url);
} else {
console.log('done');
}
});
}
getAndExtract(url);
So maybe our version of get
will now just give back a Promise to wrap the async code. →
function get(url) {
return new Promise(function(fulfill, reject) {
console.log('getting ', url);
req = new XMLHttpRequest();
req.open('GET', url, true);
req.addEventListener('load', function() {
if(req.status >= 200 && req.status < 400) {
fulfill(req.responseText);
} else {
reject('got bad status code ' + req.status);
}
});
// also reject for error event listener!
req.send();
});
}
function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}
const url = 'http://localhost:3000/data/tango.json';
get(url)
.then(extractURL)
.then(get)
.then(extractURL)
.then(get)
.then(extractURL)
.then(function(val){
console.log(val);
console.log('done');
});
So, promises are kind of complicated, but in the end, they do simplify things. Some things that we didn't cover that further show the power of using the Promise API are:
error handling
having code trigger only when multiple async tasks are finished (rather than just one)
Using Promise
s seemed to complicate AJAX rather than make it easier.
It's certainly tricky manually wrapping async tasks with Promises, but:
fortunately for us, we'll mostly encounter Promises as the result of using some built-in functions in JavaScript, like fetch →
the fetch
api provides a global function fetch
that allows the retrieval of a url
but wraps the result of that retrieval in a Promise:
fetch(url)
.then(function(response) { return response.text(); })
.then(handleResponse)
The Fetch API offers a Request
and Response
object, as well as a global fetch
function →
The fetch
function:
takes one argument, a url
it returns a promise
the promise is resolved with Response
object
fetch('http://foo.bar.baz/qux.json')
.then(function(response) {
# Response object!
return response.json();
})
.then(function(data) {
console.log(JSON.stringify(data));
});
async
and await
An alternative to using then
is to use async
and await
await
will wait for the Promise
on the right to resolve… and the await
expression will be evaluated to the value that the Promise
is resolved with
await
can only be used at the top level of your code outside of a function
… or within a function declared as async
it allows async operations to run sequentially
async
declared functions are functions that are explicitly marked as asynchronous
they return a promise
they allow await
to be used
.then
to async
and await
As an example of using async and await, we have this code:
fetch('http://foo.bar.baz/qux.json')
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log(JSON.stringify(data));
});
Which can be converted to:
const response = await fetch('http://foo.bar.baz/qux.json');
const data = await response.json();
console.log(data);
(note that the await
code above must be in an async
declared function or at the top-level)
The Response
object has a few methods and properties →
status
headers
json()
- response body parsed json
text()
- response body
A fetch
call can be configured by passing in an object as the second argument →
// example of options for a POST
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: 'foo=bar'
}
Check out mdn's Using Fetch for more detailed examples.