You CAN Convert your Callbacks to Promises
Steve Griffith
Posted on March 15, 2021
Callback functions have been part of JavaScript since the start, and to be clear, I don't think that there is anything wrong with callback functions. They serve a purpose and have done so well. I still use callbacks regularly.
I have even posted videos about what callback functions are and how you can use them in your code. Here are a couple examples:
The problem that some developers have with callbacks is known as callback hell
. This happens when you end up nesting multiple callbacks inside of each other.
Here is a completely fabricated example to give you an idea of what I mean.
myObject.someTask((returnObj) => {
//this is the success callback
//our `returnObj` is an object that also has a method
//which uses a callback
returnObj.otherTask( (otherObj) => {
//successfully ran `otherTask`
//the `otherObj` sent back to us
// has a method with callbacks
otherObj.yetAnotherTask( (anotherObj) => {
//success running yetAnotherTask
// we are reaching callback hell
// imagine if anotherObj had a method
// which used callbacks...
},
(error)=>{
//failed to run yetAnotherTask
}
},
(error)=>{
//failed to run otherTask
}); //end of otherTask
},
(error)=>{
//this is the error callback
}); //end of someTask
The goal of the code above is to run myObject.someTask( )
. When that is finished we want to run returnObj.otherTask( )
which uses the object returned from someTask
. After otherTask
runs we want to call otherObj.yetAnotherTask( )
.
I'm sure you get the point here.
Just because we wanted to run these three methods in order, we ended up creating this large group of nested curly-braces and function calls.
The code runs fine. There are no errors. But the nested sets of parentheses and curly-braces make it easy to make typos and make it difficult to read.
The Promise Difference
With Promises
we can turn a series of tasks into something that is alot easier to read. Each task gets its own then( )
method as a wrapper and we can chain them together.
Promise.resolve()
.then(()=>{
//first task
})
.then((returnedValue)=>{
//second task
})
.then((returnedValue)=>{
//third task
})
.catch((error)=>{
//handle errors from any step
})
Wrap that Callback
Now, while we can't take a built-in function like navigator.geolocation.getCurrentPosition( )
and change the native code to turn it into a Promise
, we CAN wrap it in one to create a utility function that we use in all our projects.
The Basic Promise Syntax
When we create a Promise, we use the new
operator and provide a function which has two arguments: one to be called when resolving the promise; and one to be called when rejecting the promise.
let p = new Promise( (resolve, reject) => {
//This function is passed to the newly created Promise.
//if we do this:
resolve();
// we are saying that the Promise worked
//if we do this:
reject();
// we are saying that the Promise failed
});
Inserting our Callback Function
We now need to place our original callback function inside the resolve-reject function, within the Promise.
let p = new Promise( (resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
//success
resolve(position);
},
(err) => {
//failed
reject(err);
});
});
The result of our geolocation call is now a Promise
object inside our variable p
. We can chain then()
and catch()
methods on the end of it, like this:
p.then( (position)=>{
console.log(position.coords.latitude, position.coords.longitude)
})
.catch( (err)=>{
console.log(err); //the error from the geolocation call
})
We now have a functional solution which, at the top level, uses a promise instead of the callback.
However, we are not doing anything with the options object and we have not really made something that would be friendly to use in our future projects.
Reusable Context
To be able to reuse our cool location Promise and not repeat ourselves, we should wrap this code in a function.
The function should include a test for browser support for geolocation too.
const getLocation = () => {
//check for browser support first
if('geolocation' in navigator){
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
//success
resolve(position);
},
(err) => {
//failed
reject( err );
}
);
});
}else{
let err = new Error('No browser support for geolocation');
return Promise.reject(err);
}
}
If the browser lacks the support for geolocation then we should return a failed promise that holds an error object.
Now, we can call our getLocation function and chain the then
and catch
methods on it.
getLocation( )
.then( pos => {
//success. We have a position Object
})
.catch( err => {
console.log(err); //the error from the geolocation call
});
Add Support for Parameters
So, we have a Promise-based call for geolocation
but we still can't customize the options parameter for our getCurrentPosition
call.
We need to be able to pass an options object to our getLocation function, like this:
let options = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
}
getLocation(options).then( ... ).catch( ... );
Inside our getLocation function we can test to see if the parameter is passed in, provide a default set of values, and then pass it to the getCurrentPosition
method as the third parameter.
const getLocation = (opts) => {
if('geolocation' in navigator){
opts = opts ? opts: {
enableHighAccuracy: false,
timeout: 10000,
maximumAge: 0,
};
navigator.geolocation.getCurrentPosition(
(position) => {
resolve(position); //success
},
(err) => {
reject( err ); //failed
},
opts
); //opts is the third argument
});
}else{
//...same as before
}
}
A ternary statement is a great way to check if one was passed in and if not, give it default values. An alternative way is to use destructuring with default values. (But that is an article for another day.)
Make Mine a Module
If you are already using the ES6 Module syntax to import your utility functions, like this one, into your websites and projects, then we can do the same thing with this approach.
Take our finished function declaration and expression and put it into a file called utils.js
.
//utils.js
const getLocation = (opts) => {
if ('geolocation' in navigator) {
opts = opts ? opts : {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 30000,
};
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve(position); //success
},
(err) => {
reject( err ); //failed
},
opts
);
});
} else {
let err = new Error('No browser support for geolocation');
return Promise.reject(err);
}
};
export { getLocation };
As the last line in this file we export our cool new Promise-based geolocation solution.
Then, back in our main JavaScript file for our website we import our code so we can use it.
//main.js
import { getLocation } from './util.js';
document.body.addEventListener('click', (ev)=>{
//click the page to get the current location
let options = {
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 0,
};
getLocation(options)
.then((pos) => {
//got the position
console.log('Latitude', pos.coords.latitude);
})
.catch((err) => {
//failed
console.warn('Reason:', err.message);
});
});
And that's everything. We now have a previously callback-only bit of code that we have made run is it were a Promise-based method.
You can follow this approach with any callback methods and build your own library of promise-based utility functions.
Remember that Chrome now requires HTTPS to test geolocation functionality. If you are testing this code over localhost, Firefox still lets you run it without HTTPS.
If you want to learn more about Promises, Javascript or practically any web development topic: please check out my YouTube channel for hundreds of video tutorials.
Posted on March 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.