39

Think of how Rails, e.g. allows you to define a property as associated with another:

class Customer < ActiveRecord::Base
  has_many :orders
end

This does not set up a database column for orders. Instead, it creates a getter for orders, which allows us to do

@orders = @customer.orders

Which goes and gets the related orders objects.

In JS, we can easily do that with getters:

{
   name: "John",
   get orders() {
     // get the order stuff here
   }
}

But Rails is sync, and in JS, if in our example, as is reasonable, we are going to the database, we would be doing it async.

How would we create async getters (and setters, for that matter)?

Would we return a promise that eventually gets resolved?

{
   name: "John",
   get orders() {
     // create a promise
     // pseudo-code for db and promise...
     db.find("orders",{customer:"John"},function(err,data) {
        promise.resolve(data);
     });
     return promise;
   }
}

which would allow us to do

customer.orders.then(....);

Or would we do it more angular-style, where we would automatically resolve it into a value?

To sum, how do we implement async getters?

Rodrigo5244
  • 5,145
  • 2
  • 25
  • 36
deitch
  • 14,019
  • 14
  • 68
  • 96
  • 2
    I can't think of any other way than returning a promise. – Felix Kling Mar 01 '15 at 06:20
  • Generators with `yield` maybe? – elclanrs Mar 01 '15 at 06:23
  • @elclanrs: AFAIK a getter cannot be a generator. – Felix Kling Mar 01 '15 at 06:24
  • @FelixKling, yeah, I think promises were made for this kind of thing. – deitch Mar 01 '15 at 06:30
  • @elclanrs, how would you do it with a generator with `yield`? Conceptually, they are supposed to "sync"-ify async behaviours without the performance impact. Want to write it up as an answer? – deitch Mar 01 '15 at 06:30
  • No generators in setters? I have to say, I have never tried (else, I wouldn't have this question :-) ). If @elclanrs writes it up as an answer, we can try it. – deitch Mar 01 '15 at 06:31
  • Spec is pretty clear about it: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-method-definitions – Felix Kling Mar 01 '15 at 06:35
  • Yeah, I guess not, I didn't put much thought into it. – elclanrs Mar 01 '15 at 06:37
  • I missed it in the spec there? – deitch Mar 01 '15 at 06:45
  • it works, but having a property return a new promise seems a little funky since js coders likely expect getters to be sync... – dandavis Mar 01 '15 at 06:57
  • @dandavis yeah, I just wrote up an example and came across that. Is the design wrong? I guess you could avoid getters entirely and use get functions, which you can define as returning promises or passing callbacks, but which way would make most sense? – deitch Mar 01 '15 at 07:06
  • i think returning a promise from a getter is clever, but clever is usually bad because it confuses fellow coders... – dandavis Mar 01 '15 at 07:16
  • I sometimes think the difference between an experience engineer and an inexperienced one is that the experienced one know s/he can be clever enough to confused him or herself! OK, so what would a better design approach be? – deitch Mar 01 '15 at 07:28
  • 1
    it's opinion, and maybe i'm old-fashioned for JS, but if there's a verb in there, i want to see a verb (like get) or parens in the call signature. i think that by hiding the parens, you make it look like a sync read of a RAM data object, which it's not. `customer.orders.then(....);` would be more intuitive (to me) as `customer.getOrders().then(....);`, `customer.orders.get(fnThen_or_callBack);`, `customer.orders(fnThen_or_callBack);`, or maybe just `customer.orders().then(....);`. in all of those cases, i can tell at a glance that something beyond a simple read is taking place. – dandavis Mar 05 '15 at 07:05
  • @dandavis you may be right on that. In the end, for this particular need, I basically put in an `embed` property, so that instead of getting `customer` and then `customer.orders`, when you `get` the `customer`, you indicate that you will want the `orders` and it embeds them (deep object retrieval). It is a bit of a punt on it, but I could find no way that was clean! – deitch Mar 05 '15 at 11:11
  • Oh, did I open a security hole. If a `user` is part of a `group`, then you can request to `embed` the `group` into the `user`, all is good. But there are also `account` on a `group`, so someone could "walk the tree". Rails solves it by not letting your request over the HTTP API. New problems... – deitch Mar 06 '15 at 12:24
  • @elclanrs: "Generators with yield" is just the legacy way of expressing `async/await` syntax (and the coroutine runner is often forgotten). And even then, the getter would have to return a promise; the difference is only in how that promise is consumed. – Bergi Nov 11 '15 at 00:01
  • @deitch I was wondering the same thing and stumbled here. CHeck out these two articles: jlongster.com/Taming-the-Asynchronous-Beast-with-CSP-in-JavaScript and http://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html. Those show how promises are used with generators or async functions. If the getter returns a promise, then we can `await customers.orders`, but we'd have to tell users of the API to be sure to `await` or to use `.then()`. – trusktr Mar 07 '16 at 17:41
  • @dandavis "returning a promise from a getter is clever". If it is well documented, then it might be fine. – trusktr Mar 07 '16 at 17:42
  • 1
    @trusktr that is pretty impressive. One of JS's great strengths has always been its async. One of its greatest weaknesses has always been its inability to actually do sync sanely when you need it to. This surely would help. – deitch Mar 08 '16 at 16:09

5 Answers5

30

The get and set function keywords seem to be incompatible with the async keyword. However, since async/await is just a wrapper around Promises, you can just use a Promise to make your functions "await-able".

Note: It should be possible to use the Object.defineProperty method to assign an async function to a setter or getter.


getter

Promises work well with getters.

Here, I'm using the Node.js 8 builtin util.promisify() function that converts a node style callback ("nodeback") to a Promise in a single line. This makes it very easy to write an await-able getter.

var util = require('util');
class Foo {
  get orders() {
    return util.promisify(db.find)("orders", {customer: this.name});
  }
};

// We can't use await outside of an async function
(async function() {
  var bar = new Foo();
  bar.name = 'John'; // Since getters cannot take arguments
  console.log(await bar.orders);
})();

setter

For setters, it gets a little weird.

You can of course pass a Promise to a setter as an argument and do whatever inside, whether you wait for the Promise to be fulfilled or not.

However, I imagine a more useful use-case (the one that brought me here!) would be to use to the setter and then awaiting that operation to be completed in whatever context the setter was used from. This unfortunately is not possible as the return value from the setter function is discarded.

function makePromise(delay, val) {
  return new Promise(resolve => {
    setTimeout(() => resolve(val), delay);
  });
}

class SetTest {
  set foo(p) {
    return p.then(function(val) {
      // Do something with val that takes time
      return makePromise(2000, val);
    }).then(console.log);
  }
};

var bar = new SetTest();

var promisedValue = makePromise(1000, 'Foo');

(async function() {
  await (bar.foo = promisedValue);
  console.log('Done!');
})();

In this example, the Done! is printed to the console after 1 second and the Foo is printed 2 seconds after that. This is because the await is waiting for promisedValue to be fulfilled and it never sees the Promise used/generated inside the setter.

Cameron Tacklind
  • 5,764
  • 1
  • 36
  • 45
  • 2
    TMHO, the getter is indeed properly promisified, but i think the example of a setter is not a realistic one: if the provided value is a promise - de-promise it first, before you call the set... very simple; however, if we're talking about a process in which the value-setting *inside* the set method include some async work, that's something else. Then, we better wrap the logic with an async nested function and return its return value (which is a promise). – Avi Tshuva May 19 '20 at 14:29
  • @AviTshuva There is no reason to de-promise before passing to a setter. That works just fine. Yes, a function is probably a better thing to use than a setter for doing some async work, but it's still technically possible with a setter. – Cameron Tacklind May 19 '20 at 17:25
  • but that's what i was arguing: not the possibility, but the lack of sense of it. – Avi Tshuva May 20 '20 at 10:49
  • Whether you should actually use `set`ters and `get`ters in your code is out of the scope of this question – Cameron Tacklind May 21 '20 at 00:51
  • i agree, hence what i said is in a comment; it is out of the scope, yet in practice, it's an important and relevant, tmho. – Avi Tshuva May 24 '20 at 11:35
4

As for asynchronous getters, you may just do something like this:

const object = {};

Object.defineProperty(object, 'myProperty', {

    async get() {

        // Your awaited calls

        return /* Your value */;
    }
});

Rather, the problem arises when it comes to asynchronous setters. Since the expression a = b always produce b, there is nothing one can do to avoid this, i.e. no setter in the object holding the property a can override this behavior.
Since I stumbled upon this problem as well, I could figure out asynchronous setters were literally impossible. So, I realized I had to choose an alternative design for use in place of async setters. And then I came up with the following alternative syntax:

console.log(await myObject.myProperty); // Get the value of the property asynchronously
await myObject.myProperty(newValue); // Set the value of the property asynchronously

I got it working with the following code,

function asyncProperty(descriptor) {

    const newDescriptor = Object.assign({}, descriptor);

    delete newDescriptor.set;

    let promise;

    function addListener(key) {
        return callback => (promise || (promise = descriptor.get()))[key](callback);
    }

    newDescriptor.get = () => new Proxy(descriptor.set, {

        has(target, key) {
            return Reflect.has(target, key) || key === 'then' || key === 'catch';
        },

        get(target, key) {

            if (key === 'then' || key === 'catch')
                return addListener(key);

            return Reflect.get(target, key);
        }
    });

    return newDescriptor;
}

which returns a descriptor for an asynchronous property, given another descriptor that is allowed to define something that looks like an asynchronous setter.

You can use the above code as follows:

function time(millis) {
    return new Promise(resolve => setTimeout(resolve, millis));
}

const object = Object.create({}, {

    myProperty: asyncProperty({

        async get() {

            await time(1000);

            return 'My value';
        },

        async set(value) {

            await time(5000);

            console.log('new value is', value);
        }
    })
});

Once you've set up with an asynchronous property like the above, you can set it as already illustrated:

(async function() {

    console.log('getting...');
    console.log('value from getter is', await object.myProperty);
    console.log('setting...');
    await object.myProperty('My new value');
    console.log('done');
})();
Davide Cannizzo
  • 2,826
  • 1
  • 29
  • 31
1

The following allows for async setters in proxy handlers following the convention in Davide Cannizzo's answer.

var obj = new Proxy({}, asyncHandler({
  async get (target, key, receiver) {
    await new Promise(a => setTimeout(a, 1000))
    return target[key]
  },
  async set (target, key, val, receiver) {
    await new Promise(a => setTimeout(a, 1000))
    return target[key] = val
  }
}))

await obj.foo('bar') // set obj.foo = 'bar' asynchronously
console.log(await obj.foo) // 'bar'

function asyncHandler (h={}) {
  const getter = h.get
  const setter = h.set
  let handler = Object.assign({}, h)
  handler.set = () => false
  handler.get = (...args) => {
    let promise
    return new Proxy(()=>{}, {
      apply: (target, self, argv) => {
        return setter(args[0], args[1], argv[0], args[2])
      },
      get: (target, key, receiver) => {
        if (key == 'then' || key == 'catch') {
          return callback => {
            if (!promise) promise = getter(...args)
            return promise[key](callback)
          }
        }
      }
    })
  }
  return handler
}
Luke Burns
  • 1,911
  • 3
  • 24
  • 30
0

Here's another approach to this. It creates an extra wrapper, but in other aspects it covers what one would expect, including usage of await (this is TypeScript, just strip the : Promise<..> bit that sets return value type to get JS):

// this doesn't work
private get async authedClientPromise(): Promise<nanoClient.ServerScope> {
    await this.makeSureAuthorized()
    return this.client
}

// but this does
private get authedClientPromise(): Promise<nanoClient.ServerScope> {
    return (async () => {
        await this.makeSureAuthorized()
        return this.client
    })()
}
YakovL
  • 7,557
  • 12
  • 62
  • 102
-4

Here's how you could implement your get orders function

function get(name) {
    return new Promise(function(resolve, reject) {
        db.find("orders", {customer: name}, function(err, data) {
             if (err) reject(err);
             else resolve(data);
        });
    });
}

You could call this function like

customer.get("John").then(data => {
    // Process data here...
}).catch(err => {
    // Process error here...
});
  • Does not take advantage of the property descriptor get/set - this would simply be adding your own method onto an object which is similar in some ways but different. – realisation Feb 14 '18 at 20:36