You CAN Convert your Callbacks to Promises

prof3ssorst3v3

Steve Griffith

Posted on March 15, 2021

You CAN Convert your Callbacks to Promises

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 
Enter fullscreen mode Exit fullscreen mode

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
  })
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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);
        });
});
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  });
Enter fullscreen mode Exit fullscreen mode

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( ... );
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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);
    });
});
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
prof3ssorst3v3
Steve Griffith

Posted on March 15, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related