Keep object chainable using async methods

async/await chaining javascript
promise chaining
async class method javascript
chain async methods
convert promise chain to async/await
chain async functions react
node chain asynchronous functions
async/await fetch

Let's say I have a class Test with around 10-20 methods, all of which are chainable.

In another method, I have some asynchronous work to do.

let test = new Test();
console.log(test.something()); // Test
console.log(test.asynch()); // undefined since the async code isn't done yet
console.log(test.asynch().something()); // ERROR > My goal is to make this 

Since every other method is chainable, I feel like it would be weird for the user if this sole method isn't.

Is there a way for me to maintain the chainable theme of my Class?


I have already thought of passing the next method in a callback function inside this method's parameter, but it's not really chaining.

test.asynch(() => something())

Same thing with Promises, it's not really chaining.

test.asynch().then(() => something())

The result I want is

test.asynch().something()

Here is a snippet that demonstrates my issue :

class Test {
  /**
   * Executes some async code
   * @returns {Test} The current {@link Test}
   */
  asynch() {
    if (true) { //Condition isn't important
      setTimeout(() => { //Some async stuff
        return this;
      }, 500);
    } else {
      // ...
      return this;
    }
  }

  /**
   * Executes some code
   * @returns {Test} The current {@link Test}
   */
  something() {
    // ...
    return this
  }
}

let test = new Test();
console.log(test.something()); // Test
console.log(test.asynch()); // undefined
console.log(test.asynch().something()); // ERROR > My goal is to make this work.

I doupt that it is a really good idea to do something like that. But using a Proxy would allow to create such a beahviour if the original Object meets certain conditions. And I would highly recommend to not do it that way.

Be aware that this code is a proof of concept to show that it is somehow possible, but doesn't care about edge cases and most likely will break certains functionalities.

One Proxy is used to wrap the original class Test so that is is possible to patch each of is instance to make them chainable.

The second one will patch each function call and creates a queue, for these functions calls so that they are called in order.

    class Test {
      /**
       * Executes some async code
       * @returns {Test} The current {@link Test}
       */
      asynch() {
        console.log('asynch')
        return new Promise((resolve, reject) => setTimeout(resolve, 1000))
      }

      /**
       * Executes some code
       * @returns {Test} The current {@link Test}
       */
      something() {
        console.log('something')

        return this
      }
    }


    var TestChainable = new Proxy(Test, {
      construct(target, args) {
        return new Proxy(new target(...args), {

          // a promise used for chaining
          pendingPromise: Promise.resolve(),

          get(target, key, receiver) {
            //  intercept each get on the object
            if (key === 'then' || key === 'catch') {
              // if then/catch is requested, return the chaining promise
              return (...args2) => {
                return this.pendingPromise[key](...args2)
              }
            } else if (target[key] instanceof Function) {
              // otherwise chain with the "chainingPromise" 
              // and call the original function as soon
              // as the previous call finished 
              return (...args2) => {
                this.pendingPromise = this.pendingPromise.then(() => {
                  target[key](...args2)
                })

                console.log('calling ', key)

                // return the proxy so that chaining can continue
                return receiver
              }
            } else {
              // if it is not a function then just return it
              return target[key]
            }
          }
        })
      }
    });

    var t = new TestChainable
    t.asynch()
      .something()
      .asynch()
      .asynch()
      .then(() => {
        console.log('all calles are finished')
      })

How To Use Async and Await to Chain Promises - Better , fields object has a dirty property, so we can check if the fields are manipulated or not. In the save function of the methods object, we have: async  The most common case is for the MoveNextAsync method to actually complete its operation synchronously, in which case the runtime would be able to use a cached task object: Every time it completes synchronously to return a true value, the same already-completed-with-a-true-result Task<bool> could be returned, making the synchronously completing case allocation-free.

I don't think that is it possible to use such syntax for now. It would require to access the promise in the in the function to return it.

Different ways to chain functions:

Promise with then

bob.bar()
    .then(() => bob.baz())
    .then(() => bob.anotherBaz())
    .then(() => bob.somethingElse());

And you could also use compositions, to obtain another style of functional, reusable syntax to chain async and sync functions

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

Or using async / await

for (const f of [func1, func2]) {
  await f();
}

Chained APIs with asynchronous functions in Javascript, Why chained async methods can be unintuitive at first synchronous functions, you just put all the functions on an object and you make sure We keep appending to this chain of promises as we evaluate the chain in the API. The async keyword represents a hint that you can use to mark methods as task-based asynchronous methods. The combination of await, async, and the Task object makes it much easier for you to write asynchronous code in .NET 4.5. The new model for asynchronous methods is called the Task-based Asynchronous Pattern (TAP).

As discussed in comments to OP, this can be accomplished by using Proxy.

I recognize that t.niese provided a similar answer a few hours ago. My approach differs somewhat, but it's still substantively trapping method calls, returning the receiver and internally stacking up thennables.

class ProxyBase {

    constructor () {

        // Initialize a base thennable.
        this.promiseChain = Promise.resolve();

    }

    /**
     * Creates a new instance and returns an object proxying it.
     * 
     * @return {Proxy<ProxyBase>}
     */
    static create () {

        return new Proxy(new this(), {

            // Trap all property access.
            get: (target, propertyName, receiver) => {

                const value = target[propertyName];

                // If the requested property is a method and not a reserved method...
                if (typeof value === 'function' && !['then'].includes(propertyName)) {

                    // Return a new function wrapping the method call.
                    return function (...args) {

                        target.promiseChain = target.promiseChain.then(() => value.apply(target, args));

                        // Return the proxy for chaining.
                        return receiver;

                    }

                } else if (propertyName === 'then') {
                    return (...args) => target.promiseChain.then(...args);
                }

                // If the requested property is not a method, simply attempt to return its value.
                return value;

            }

        });

    }

}

// Sample implementation class. Nonsense lies ahead.
class Test extends ProxyBase {

    constructor () {
        super();
        this.chainValue = 0;
    }

    foo () {
        return new Promise(resolve => {
            setTimeout(() => {
                this.chainValue += 3;
                resolve();
            }, 500);
        });
    }

    bar () {
        this.chainValue += 5;
        return true;
    }

    baz () {
        return new Promise(resolve => {
            setTimeout(() => {
                this.chainValue += 7;
                resolve();
            }, 100);
        });
    }

}

const test = Test.create();

test.foo().bar().baz().then(() => console.log(test.chainValue)); // 15

Async Programming, Even in synchronous code, a causality chain and return stack aren't always identical. Luckily, if asynchrony is introduced into a program by using async and await post-mortem debugging, you have to explicitly preserve them along the way. method in an event source are boxed into a read-only object collection and  Making a sync operation Task -compatible. Most async methods return Task, but not all Task -returning methods are necessarily async. That can be a bit of a mind-bender. Let's say you need to implement an a method that returns Task or Task <TResult>, but you don't actually have anything async to do.

Graceful asynchronous programming with Promises, Essentially, a Promise is an object that represents an intermediate state As long as the app doesn't assume that streaming has begun, it can just keep on running. We're able to chain multiple asynchronous actions to occur one after Note: You can make further improvements with async / await syntax,  The core of async programming is the Task and Task<T> objects, which model asynchronous operations. They are supported by the async and await keywords. The model is fairly simple in most cases: For I/O-bound code, you await an operation which returns a Task or Task<T> inside of an async method.

25. Promises for asynchronous programming, then() always returns a Promise, which enables you to chain method calls: with callbacks: Now an asynchronous function returns a Promise, an object that is a new Promise Q. That means that you can keep the Promise-based control flow  The simplest use of "async/await" is to use the "await" option, then continue with your code afterwards. However, the .NET task object provides other methods of continuation once the asynchronous task has completed. Running Multiple Tasks/Chaining Tasks. If you need to run multiple SQL operations, there are a few ways of achieving this.

Async functions, Async return values. Async functions always return a promise, whether you use await or not. That promise resolves with whatever the async  Set the context used by async-chainable during subsequent function calls. In effect this sets what this is for each call. Omitting an argument or supplying a 'falsy' value will instruct async-chainable to use its own default context object.

Comments
  • What about using promises, like there ? : stackoverflow.com/questions/39028882/…
  • @NanoPish It still breaks the chaining, test.asynch().something() would become test.asynch().then(() => something())
  • reading the answer to the question I linked, he links 3 functions using promises, it seems to resolve your issue ?
  • @Zenoo, unfortunately, what you're describing isn't possible. At least, as far as I know. At best it would produce confusing code to anybody else reading it, however, as that is pretty far from idiomatic. You're familiar with using async/await, I assume? That's about as close as we can get to treating asynchronous code as synchronous.
  • @Zenoo hmm... well now I think I've caught a case of your curiosity. I wondered if await would extend to chained executions, but it seems it does not: jsfiddle.net/7v5y9jck. You might be able to create a class that offloads every method invocation into a queue and return the instance itself. Actually, you could probably use Proxy for that, and then include a then method that ultimately resolves as your final result.
  • Why do you advise not to do it? Do you have some examples for what could break using this code? Finally, do you think it would have a bad impact performance wise?
  • @Zenoo It was/is mainly a vague premonition due to many projects and ideas, where problems occuered at a really late stage. One problem that I see right now is that each newly started chaining will use the last Promise of the previous chain. So if the previous chain failed and the error was not catched then the newly started chain will fail immediatly. To overcome this you need to create an own Proxy for each started chain, but then you need to need to make sure that those chains don't interleave.
  • @Zenoo and I'm sure there will be more problems that will become visible later. You will at least need to speed a long time to test with many edge cases.
  • Thank you, I don't know if I'll end up using it, but at least you proved it was possible. I will accept @t.neise answer though, since it was the first one posted and it handles the catch too.
  • That would have been my suggestion as well. Cheers.