vtomic85
vtomic85

Reputation: 611

JavaScript promise with .then inside .then doesn't work

Current state:

I have the following function in a link builder which is using a configuration service which returns a promise with some enviroment parameters:

_createLink: function (label, args) {
   return LinkBuilder.builder()
      .withLabel(label)
      .withUrl(ConfigService.getConfig()
         .then((env) => env.baseUrl 
                      + TranslationService.instant('MY.URL', args)))
      .buildLink();
},

A few explanations:


    withUrl: function (param) {
                        if (typeof param === 'string') {
                            builder.value.linkUrl = param;
                        } else if (typeof param === 'function') {
                            builder.value.linkCalculationFunction = param;
                        } else if (typeof param === 'object' && typeof param.lazyBuildUrl === 'function') {
                            builder.value.linkCalculationFunction = param.lazyBuildUrl();
                        } else if (typeof param === 'object' && typeof param.then === 'function') {
                            builder.value.linkCalculationFunction = () => param;
                        } else {
                            throw new Error('invalid url param ' + param);
                        }
                        return builder;
                    },

withLabel: function (label) {builder.value.labelKey = label; return builder;}

buildLink: function () {return builder.value;}

Problem:

I need to encode those args using an external service which also returns a promise and then to append the return value to my link. That external service will return something like 'data': {'encodedId': '123-456-789'}, and my final link should be: http://www.myserver.com#encodedId=123-456-789. Here is what I did:

My original attempt:

In the JSON file with translations I removed the parameters from the entry, so now it's only: 'http://www.myserver.com#'

The _createLink function now looks like this:

_createLink: function (label, args) {
   let content = {
      'name': args.name,
      'age': args.age
   };
   return EncodingService.saveContent(content)
       .then((data) => {
           return LinkBuilder.builder()
              .withLabel(label)
              .withUrl(ConfigService.getConfig()
                  .then((env) => env.baseUrl 
                               + TranslationService.instant('MY.URL') 
                               + 'encodedId=' + data.encodedId))
              .buildLink();
},

When I place a breakpoint at the last then line, I see that I have the correct data.encodedId, the correct env.baseUrl and the TranslationService is also correctly returning the JSON entry. But for some reason, I don't see the link in my page anymore. I guess that I'm doing something wrong with chaining the .thens, but I'm not sure what.

Another attempt which still doesn't work:

Here is the third version, which still doesn't work:

_createLink: function (label, args) {
       let content = {
          'name': args.name,
          'age': args.age
       };
       return $q.all([
          EncodingService.saveContent(content),
          ConfigService.getConfig()
       ])
           .then((data) => {
               let url = data[1].baseUrl
                       + TranslationService.instant('MY.URL') 
                       + 'encodedId=' + data[0].encodedId;
               return LinkBuilder.builder()
                  .withLabel(label)
                  .withUrl(url)
                  .buildLink();
    },

Again, I placed the breakpoint at the last return statement and there I have the url formed correctly...

Usage of `_createLink`:

This function is used like this:

getLeadLink: function (label, access, address) {
   return myService._createLink(label, someService._createArguments(access, address));
        },

...and getLeadLink is used in a component like this:

this.$onInit = () => {
...
   this.leadLink = OtherService.getLeadLink('LINEINFO.LEAD.LINK', someData.access, someData.address);
...
};

...and leadLink is then displayed in the HTML file.

Note: None of these things was modified, so the usage of _createLink and the displaying of the link is still the same as it was before.

Upvotes: 0

Views: 107

Answers (1)

trincot
trincot

Reputation: 350034

The third attempt is going in the right direction.

There is however no way you can get the future URL now. And so any attempt that in the end expects a simple function call to return the URL cannot be right. Such a function (like _createLink) is deemed to return before the asynchronous parts (i.e. any then callbacks) are executed. In your third attempt you improved things so that at least _createLink will return a promise.

Now remains to await the resolution of that promise.

So, any wrapping function execution around _createLink, should take into account the asynchronous pattern:

getLeadLink: function (label, access, address) {
   return myService._createLink(label, someService._createArguments(access, address));
},

This is fine, but realise that _createLink does not return the URL, but a promise. And so also getLeadLink will return a promise.

You write that you want to display the URL, so you would do something like this (I don't know how you will display, so this is simplified):

OtherService.getLeadLink('LINEINFO.LEAD.LINK', someData.access, someData.address).then(function(builder) {
    var $link = $("<a>").attr("href", builder.value.linkUrl)
                        .text(builder.value.labelKey);
    $(document).append($link);
});

The key message is: don't try to get the URL as the return value of a function. It does not matter how many wrappers you write around the core logic; it will remain an asynchronous task, so you need to get and display the URL asynchronously as well. You'll need to do it in a then callback, or use await syntax, which makes anything following it asynchronous code.

Upvotes: 2

Related Questions