DEV Community

Cover image for You CAN Convert your Callbacks to Promises
Steve Griffith
Steve Griffith

Posted on • Updated on

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.

Latest comments (6)

Collapse
 
rowild profile image
Robert Wildling

Very nice explanation! And I like it that you used the geolocation API for an example. May I ask 2 things, please:

  1. Wouldn't it be better to put the "geolocation in navigator" check outside of the actual getGeolocation method? Now this check is called every time when I click on the screen, which seems superfluous, since I already checked it. To me it seems to make more sense to offer the button (or the document.addeventListener) only, when geolocation is available. That way the check would only happen once – and consequently make the UI cleaner, since there wouldn't be a button for calling the geolocation.

  2. Is there any chance you could make a tutorial on how to use the "maximumAge" parameter of the geolocation's options object? I tried to make sense of it, but... my brains stays dark...
    I thought the idea of the maximumAge param is to set a time value (like 10 secs), that caches a position value, so all the next values that the device recognizes, have no efect, because within the 10 seconds the cached values are taken. That way I have a way to save less position coords in a tracking app, e.g. (Because if I save any and all coordinate, the data would explose in size really quick!)
    However, it seems it does not work at all like that. Whenever I save a value (reduced to 6 post-comma positions) to an oldVal var and compare it with the new incoming val, I get new data all the time. Never a cached one.

Would be awesome if you could shed light on that feature!
Thanks!

Collapse
 
prof3ssorst3v3 profile image
Steve Griffith

Thanks for the comments and questions. :)
I have a video tutorial on my YouTube channel all about Geolocation - youtube.com/watch?v=NIAqR34eg7I . There are other geolocation tutorials with Google maps, Cordova, and React Native too, but this is the first one focused on just geolocation.
Any time you are going to use any API you want to put the check for the feature as close as possible to the actual use as you can. You never know where your function module may end up. So, you should keep the code all bundled together so it can be moved together.
If you want to avoid the check, which really is only taking a fraction of a millisecond to check for the existence of a property inside an object, then you could create a variable outside the function, still inside the module, to save the result. But then you need to check the value of that variable. Either check would take the same amount of time but the second way requires using extra memory to save the value.

Collapse
 
rowild profile image
Robert Wildling

Thank you for your answer! I need to explain my situation a bit more. First of all, I am talking about the "watchGeolocation" option, not the "getCurrentPosition" option that you use in your other tutorials. (BTW: I know that there are many tutorials out there, but it seems none talks about the proper usage of "maximumAge".)
In my understanding, when I set maximumAge to e.g. 5000, then all incoming GPS signals within the next 5 seconds should have the same timestamp, since they would be fetched from the cache. Right? But this is not the case.

I setup an example here:
geolocation-maximumage-test.vercel...
The repo is here:
bitbucket.org/rowild/geolocation-m...
(Sorry for the meaningless git messages...)

This example shows clearly that the timestamp changes whenever a GPS signal is recognised (about every second).

So I wonder: What the heck is this maximumAge value doing really?

The idea is to build a tracking app. The user should set a maximumAge (a higher one, when the activity is walking, a lower one, when the activity is biking etc.. the usual stuff...), so the app does not save each and every incoming GPS value. I would set the first new value to a var called "cachedTimestamp" and compare all the next incoming values with that cached timestamp. That would enable me to save a new value only every 30 seconds (e.g.).
But since the timestamp changes all the time, this is not the way to go.

Any idea what is going n here?

Thank you very much!

Thread Thread
 
prof3ssorst3v3 profile image
Steve Griffith • Edited

maximumAge doesn't keep the same timestamp with each request.
The browser will cache a coordinates object with a timestamp.
Each subsequent time you make a request, it will look at the current timestamp and compare it with the cache result's timestamp. If you have not reached the maximumAge yet then you get a copy of the coordinates object along with a current timestamp.
The position object
{
timestamp: current value for the current request
coords: { this could be the cached coords object or a new one if you are past the cached timestamp }
}
HOWEVER...
The browser is allowed to adjust the result in the interest of better battery life and lowered use of the radio antenna in the device.
It also uses the highAccuracy setting to decide what to do based on other things happening on the device. When you say that you want highAccuracy, you are really just saying that it is allowed for the device to use it, not that the device MUST use it.
The developer provides their best intention to the browser through the options object, but it is still up to the browser to interpret those settings.
Many of the HTML5 APIs work this way.

Thread Thread
 
rowild profile image
Robert Wildling

Thank you again very much, Steve!

What I observe is that the coordinate values change all the time, no matter what. So if I understand you correctly: Using maximumAge to manage the amount of geolocation points to be saved to a data store is unreliable. If I want to save a geolocation datum only every 3rd second; i need to implement my own timer.
To be honest: I do not really understand, what this setting is good for, if the browser does whatever it wants whenever it wants... :-(

However, I love your tutorials and texts and am looking forward to new stuff of yours! Thank you again! Have a good day!

Thread Thread
 
prof3ssorst3v3 profile image
Steve Griffith

You're very welcome.
Just bear in mind that changing the maximumAge option, the enableHighAccuracy option, or using a timer you create yourself will get similar results. The management of the radio in the phone is beyond absolute control of the JS. It will always be the device and the browser that get to make the final decision about how to use the radio - wifi, gps, etc.