Callbacks & promises in JS for newbies
You are a newbie in JavaScript? You want to know a little bit more about asynchronous nature of this language? Join me!
Problem
I was working on my project that is using Google Maps. I stumbled upon an issue - how I display a map after submitting a form with geocoded address in it?
After some research, I found that I need first geocode and then if it passes to render the map with a custom marker.
I did this and it doesn’t work! I try one more time and one more. After one hour I decided to write a question on stackoverflow.
Guys there flagged it as a duplicate and told me you should look into how to handle an asynchronous call. It was confusing - how this is near rendering a map after submitting a form?
I dig more and ask a question on reddit. Then I got my answer - use callback or promise!
Solution
Based on an answer from adavidmiller from reddit I was able to write this code:
function geocodeAddress(address, callback) {
const geocoder = new google.maps.Geocoder();
geocoder.geocode(
{
address,
},
function (results, status) {
if (status === "OK") {
callback({
lat: results[0].geometry.location.lat(),
lng: results[0].geometry.location.lng(),
});
} else {
alert("Cannot find address");
}
},
);
}
There is a extra argument to this function called callback
. It will be
executed if everything goes properly. What can be this callback? Look at
this:
function onGeocodeComplete() {
const map = new google.maps.Map(mapElem, {
zoom: 4,
center: coords,
});
const marker = new google.maps.Marker({
position: coords,
map,
});
}
In this function, I create a new map that is centered in coords
. How
will JavaScript know local variable coords
? Because it is callback
passed to geocodeAddress
:
function initMap() {
searchForm.addEventListener("submit", (event) => {
event.preventDefault();
questionForm.classList.add("is-hidden");
const place = searchForm.querySelector("[name=place]").value;
geocodeAddress(place, onGeocodeComplete);
});
}
Look at the last line in this function - this is a place where I pass
address
which is value from submitted form and I pass callback
which
is whole onGeocodeComplete
function.
You may say ‘It looks neat - let’s use callbacks! but in many cases this
can lead to so called callback hell. To
avoid that in ES6 is new way to handle asynchronous JavaScript -
Promise
.
The same function - geocodeAddress
written using promise:
function geocodeAddressPromise(address) {
return new Promise((resolve, reject) => {
const geocoder = new google.maps.Geocoder();
geocoder.geocode(
{
address,
},
(results, status) => {
if (status === "OK") {
resolve({
lat: results[0].geometry.location.lat(),
lng: results[0].geometry.location.lng(),
});
} else {
reject("Cannot find address");
}
},
);
});
}
The code looks almost the same as in callback but this function returns promise. If the promise was resolved - a.k.a everything goes smoothly it will return lat & lng. In other case information about the error will be returned.
How to handle such promise? You can use that:
function initMap() {
searchForm.addEventListener("submit", (event) => {
event.preventDefault();
questionForm.classList.add("is-hidden");
const place = searchForm.querySelector("[name=place]").value;
geocodeAddressPromise(place).then(
(response) => {
onGeocodeComplete(response);
},
(error) => {
alert(error);
},
);
});
}
I resolve a promise by calling then
on it. I pass here arrow function
with a response if the promise was resolved. In another case, I
display an error to the user.
What I’ve learn
I learn quite a lot from having this kind of problem:
- what does it mean to write and use asynchronous code in Javascript
- what is difference between callback & promise
- how to write code using both callback & promise
That’s all for today! Feel free to comment - all are realy valuable for me. I want to thank one more time adavidmiller for taking his time to show me how to write code using callbacks.
Update 10.07.2017
As Daniel Levy pointed out in comment you
can refactor my code, so geocodeAddressPromise
will look like this:
// Updated code for https://krzysztofzuraw.com/blog/2017/callbacks-promises-in-js-for-newbies.html
const Promise = require('bluebird');
const geocoder = new google.maps.Geocoder();
// Most JS API's let you use Bluebird.promisify[All]
// Unfortunately Google's APIs are a little dumb when it comes to promises and Node callbacks.
const geocodeAddressPromise = (address) => new Promise((resolve, reject) => geocoder
.geocode({ address }, (results, status) => status === 'OK'
? resolve({
lat: results[0].geometry.location.lat(),
lng: results[0].geometry.location.lng(),
}) : reject(new Error('Cannot find address'))
}));
New thing here is a better promise API - bluebird as well as more javascripty syntax.
onGeocodeComplete
returns both map
and marker
:
function onGeocodeComplete(coords) {
const map = new google.maps.Map(mapElem, {
zoom: 4,
center: coords,
});
const marker = new google.maps.Marker({
position: coords,
map,
});
return { map, marker };
}
The last thing is initMap
:
function initMap() {
searchForm.addEventListener("submit", (event) => {
event.preventDefault();
questionForm.classList.add("is-hidden");
const place = searchForm.querySelector("[name=place]").value;
geocodeAddressPromise(place).then(onGeocodeComplete).catch(alert);
});
}
Which will catch errors along the way. All credits for this update goes to Daniel. Thanks man!