You CAN Convert your Callbacks to Promises
Tags: javascript, callbacks, promises, webdev
Published: 2021-03-15 19:36:28

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:

{% youtube uPCxgnLOuiQ %}

{% youtube 0pNo1O42m6k %}

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.

```js
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.

```js
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.

```js
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.

```js
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:

```js
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.

```js
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.

```js
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:

```js
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.

```js
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`.

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

```js
//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](https://www.youtube.com/c/SteveGriffith-Prof3ssorSt3v3/videos) for hundreds of video tutorials.