knowt logo

Managing Callbacks / Promises CSCI-UA.0467-002

Managing Callbacks / Promises

CSCI-UA.0467-002

Asynchronous Tasks and Dependencies

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

A Long Running Function

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
);
}

When Does 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!!!

Modify 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
);
}

Using our new 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'));

What About Errors?

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
);
}

Errors Continued

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.

Instead of try/catch…

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!

Promises

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.

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

A Promise Object Continued

Again, a Promise is an object that represents an async task →

Consequently, a Promise can be in one of the following states:

  1. pending - the task hasn't been completed yet (still getting a url, reading a file, etc.)

  2. fulfilled - the task has completed successfully

  3. 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

Creating a Promise

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 Methods

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 and Success / Fulfill

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

An Immediately Fulfilled 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!

Immediately Fulfilled Continued

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);

When is Fulfill or Reject Executed?

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 Argument

To 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);
});

Back to Then!

then always returns a Promise →

  • if the fulfill function returns a Promisethen 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 Value

Starting 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

Fulfill not Returning a Promise?

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

Wrapping a Value in 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;
});

Another Example (Though Frontend Code)

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);
});

No Big Deal

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

Async Tasks All the Way Down

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. ▲ ▲ ▲ ▲ ▲

A Tiny Pyramid Made of HTTP Requests

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");
});
});
});

Let's Actually Try This

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… →

  1. Create an express app to serve up our files…

  2. 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{}

  3. Create a page that uses external JavaScript that…

  4. Uses XMLHttpRequest to pull retrieve tango.json

  5. Extract the url, and retrieve it… and do the same for the third url…

This is Going to be Ugly

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();

Obviously, That Was Terrible

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)

get

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();
}

extractURL…

This one's simple

function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}

Voila (Using get and extractURL)

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

One More Function

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);

Promises with AJAX

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();
});
}

Keeping Our Extract Function…

function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}

We Can Make Async Look Sequential

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');
});

Things We Pretended Didn't Exist

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)

Promises Look Hard! / Fetch

Using Promises 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)

Fetch API Details

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));
});

Using 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

From .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)

Response Object

The Response object has a few methods and properties →

  • status

  • headers

  • json() - response body parsed json

  • text() - response body

fetch and Config

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.

Managing Callbacks / Promises CSCI-UA.0467-002

Managing Callbacks / Promises

CSCI-UA.0467-002

Asynchronous Tasks and Dependencies

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

A Long Running Function

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
);
}

When Does 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!!!

Modify 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
);
}

Using our new 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'));

What About Errors?

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
);
}

Errors Continued

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.

Instead of try/catch…

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!

Promises

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.

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

A Promise Object Continued

Again, a Promise is an object that represents an async task →

Consequently, a Promise can be in one of the following states:

  1. pending - the task hasn't been completed yet (still getting a url, reading a file, etc.)

  2. fulfilled - the task has completed successfully

  3. 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

Creating a Promise

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 Methods

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 and Success / Fulfill

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

An Immediately Fulfilled 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!

Immediately Fulfilled Continued

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);

When is Fulfill or Reject Executed?

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 Argument

To 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);
});

Back to Then!

then always returns a Promise →

  • if the fulfill function returns a Promisethen 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 Value

Starting 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

Fulfill not Returning a Promise?

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

Wrapping a Value in 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;
});

Another Example (Though Frontend Code)

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);
});

No Big Deal

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

Async Tasks All the Way Down

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. ▲ ▲ ▲ ▲ ▲

A Tiny Pyramid Made of HTTP Requests

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");
});
});
});

Let's Actually Try This

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… →

  1. Create an express app to serve up our files…

  2. 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{}

  3. Create a page that uses external JavaScript that…

  4. Uses XMLHttpRequest to pull retrieve tango.json

  5. Extract the url, and retrieve it… and do the same for the third url…

This is Going to be Ugly

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();

Obviously, That Was Terrible

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)

get

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();
}

extractURL…

This one's simple

function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}

Voila (Using get and extractURL)

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

One More Function

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);

Promises with AJAX

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();
});
}

Keeping Our Extract Function…

function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}

We Can Make Async Look Sequential

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');
});

Things We Pretended Didn't Exist

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)

Promises Look Hard! / Fetch

Using Promises 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)

Fetch API Details

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));
});

Using 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

From .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)

Response Object

The Response object has a few methods and properties →

  • status

  • headers

  • json() - response body parsed json

  • text() - response body

fetch and Config

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.

robot