Async/Await should operate on Promises/A+ spec Promises

Topics: General, Language Specification
Apr 1, 2013 at 10:32 AM
Edited Apr 1, 2013 at 9:55 PM
In C#, awaits are performed on Task objects. The most obvious allegory for Task objects in JavaScript right now are promises, and the Promises/A+ spec seems to be most popular.

So, it would look something like this:
// Note that in the following code, WinJS.Promise implicitly implements
// this interface:
interface Promise<T> {
     then<U>(onFulfilled?: (value?: T) => U, onRejected?: (reason?: any) => any): Promise<U>;
}

// Operates on standard promise objects
async function getBlogPost(id: number): WinJS.Promise<BlogPost> {
     var request = await WinJS.xhr({ url: "www.mywebsite.com/blog/post/" + id });
     return <BlogPost> JSON.parse(request.innerText);
}

// If the function returns, it must have the async modifier to indicate that
// it returns a promise.
// Try/catches can be used for the onRejected function call of the promise.
// In the output JavaScript, the code following the try catch loop would
// need to be wrapped in a function called by both onResolved and
// onRejected.
async function loadComment(id: number): WinJS.Promise<Comment> {
     var request = null;
     try {
          request = await WinJS.xhr({ url: "www.mywebsite.com/comments/" + id });
     } catch {
          request = "This comment could not be loaded."
     }
     return <Comment> JSON.parse(request.innerText);
}

// An array of promises can be return as a promise for all values.
// Unfortunately, I don't know any way to do this without adding new 
// functions like __extends. Potentially user could define a function 
// with a specific name that wraps an array of promises with their favorite
// library? (Q, WinJS, etc.)
async function loadComments(post: BlogPost): Promise<Comment[]> {
     var comments = <Comment[]>new Array(post.commentIds.length);

     for(var i = 0; i < post.commentIds.length; i++) {
          comments[i] = await loadComment(post.commentIds[i]);
     }

     return comments;
}

// Async doesn't have to be used as long as there's no return
function displayBlogPost(id: number): void {
     getBlogPost(id).then(function(post) { // Can still be used like a promise
          loadComments(post).then(function(comments) { 
               ui.append(comments);
          }
          ui.append(post);
     }
}
My reasons that this is how async/await need to be done are as follows:
  1. Don't need to add a lot of plumbing code to the output JavaScript. The compiler just looks that the return of an async function implements the implicit Promises/A+ interface, or is an array of such types. Makes it interoperable with any promise implementation as long as it's "correct".
  2. Closely matches the conceptual model people are already using with promises. i.e. that promises are just a way to think about asynchronous code in a synchronous manner.
  3. Is similar to how C# uses Task as the object await acts on.
Some possible drawbacks:
  1. Awaiting on arrays of async operations will require extra functions to be added
  2. Dealing with rejected promises could be a tricky in a way that meshes well with the synchronous model of try/catch.
And maybe you devs have already created the actual spec and it looks nothing like this. I'm just envisioning in my head waking up to TypeScript async/await support and having it be instantly useful and syntactically awesome with all of my existing libraries that use promises.

Let me know what you guys think.
Apr 1, 2013 at 6:41 PM
@AndrewGaspar, how will the JavaScript implementation of this look?
async function getBlogPost(id: number): WinJS.Promise<BlogPost> {
     var request = await WinJS.xhr({ url: "www.mywebsite.com/blog/post/" + id });
     return <BlogPost> JSON.parse(request.innerText);
}
Also assuming the calling code will look like this
function onGetBlogClick(){
  var post = await getBlogPost(5);

 var foo = new Foo();
 foo.displayPost(post);
}
Apr 1, 2013 at 7:09 PM

+1 to nabog's question. It's all very well asserting that the JavaScript plumbing code won't be complicated, but have you seen what the C# compiler has to do with await calls even in simple for loops?

Apr 1, 2013 at 9:50 PM
nabog wrote:
@AndrewGaspar, how will the JavaScript implementation of this look?
async function getBlogPost(id: number): WinJS.Promise<BlogPost> {
     var request = await WinJS.xhr({ url: "www.mywebsite.com/blog/post/" + id });
     return <BlogPost> JSON.parse(request.innerText);
}
This would translate to
function getBlogPost(id) { 
     return WinJS.xhr({ url: "www.mywebsite.com/blog/post/" + id }).then(function(request) {
          return JSON.parse(request.innerText);
     });
}
function onGetBlogClick(){
  var post = await getBlogPost(5);

 var foo = new Foo();
 foo.displayPost(post);
}
This would translate to:
function onGetBlogClick() {
     getBlogPost(5).then(function(post) {
          var foo = new Foo();
          foo.displayPost(post);
     });
}
Basically, any code following an await would be performed inside of the "onResolved" function of the promise.
Apr 1, 2013 at 10:24 PM
That's a very naive implementation, basically just syntactic sugar for a callback. Proper C# style async/await can cope with await statements in loops, using very complicated iterator-based state machines.
Apr 18, 2013 at 10:38 AM
Edited Apr 18, 2013 at 2:00 PM
That seems like a really bad idea, it gives the illusion that "onGetBlogClick" would say "foo.displayPost" before it exits, but the fact is that it dont...

Imagine:
class Foo {
    Baz: string;
    Bar() {
        this.Baz = await someservice.getBaz();
    }
    
    Boo() {
        this.Bar();
        console.log(this.Baz);
    }
}
Which would most likely result in undefined, where the programmer would go "Ehhh... what???"... Because Baz aint set until later...

I don't feel comfortable by adding the async/await keywords to TS considering that your not meant to block in JavaScript, I think it's better that people actually learn to use those promises...

I don't mind syntactical sugar around the pattern, but it should make it clear what is going on.

But then again... IS it really needed... it's not like promises are a big burden to use... They might have a learning curve, but that's about it.
Apr 21, 2013 at 10:36 PM
Edited Apr 21, 2013 at 10:36 PM
jmelgaard wrote:
class Foo {
    Baz: string;
    Bar() {
        this.Baz = await someservice.getBaz();
    }
    
    Boo() {
        this.Bar();
        console.log(this.Baz);
    }
}
This won't compile. If a method contains await then it must be marked as async and called with await. Without await, it will just return a promise.
class Foo {
    Baz: string;
    async Bar() {
        this.Baz = await someservice.getBaz();
    }
    
    async Boo() {
        await this.Bar();
        console.log(this.Baz);
    }
}
Apr 22, 2013 at 12:06 AM
Edited Apr 22, 2013 at 1:14 AM
@jmelgaard

I don't think the criticism that Baz isn't set at that log statement is fair criticism. Firstly, the compiler would reject that code because Bar is not marked as async. Secondly, since Bar is an asynchronous function, the developer should not expect Baz to be set at that log statement without "awaiting" it. For example, see the following C# code:

AsyncTest.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using System.IO;

namespace AsyncTest
{
    class TestClass {
        public int Value { get; set; }

        private Task wait()
        {
            return Task.Delay(1000);
        }

        public async Task IncrementValue() {
            await wait();
            Value++;
        }

        public TestClass()
        {
            Value = 0;
        }

        private static async Task AsyncMain()
        {
            var tc = new TestClass();
            var task = tc.IncrementValue();

            Console.WriteLine(tc.Value);

            await task;

            Console.WriteLine(tc.Value);
        }

        public static void Main()
        {
            TestClass.AsyncMain().Wait();
        }
    }
}
The output, in general, should look like:
PS E:\Personal Files\Local\Projects\temp> csc .\AsyncTest.cs
Microsoft (R) Visual C# Compiler version 4.0.30319.17929
for Microsoft (R) .NET Framework 4.5
Copyright (C) Microsoft Corporation. All rights reserved.

PS E:\Personal Files\Local\Projects\temp> .\AsyncTest.exe
0
1
A comparable TypeScript implementation would look like:

AsyncTest.ts
/// <reference path='q.module.d.ts' />

import q = module("q");

module AsyncTest {
    interface Promise {
        then(onFulfilled?: (value?: any) => any, onRejected?: (reason?: any) => any): Promise;
    }

    export class TestClass {

        constructor(public value: number = 0) { }

        private wait(): Promise {
            var def = q.defer();

            setTimeout(() => def.resolve(), 1000);

            return def.promise;
        }

        public incrementValue(): Promise {
            return this.wait().then(
                () => this.value++);
        }

        private static asyncMain() {
            var tc = new TestClass();
            var task = tc.incrementValue();

            console.log(tc.value);

            return task.then(function () {
                console.log(tc.value);
            });
        }

        public static main() {
            TestClass.asyncMain();
        }
    }
}

AsyncTest.TestClass.main();
With an output of:
PS E:\Personal Files\Local\Projects\temp> tsc AsyncTest.ts
PS E:\Personal Files\Local\Projects\temp> node AsyncTest.js
0
1
Obviously these two examples have a meaningful difference in that C# has multi-threading creating the asynchronous behavior and TypeScript's asynchronous behavior is due to the Event Loop, but I don't see how this is relevant to the purpose of async/await, which is to make reasoning about asynchronous programs easier. This syntactic sugar would be nice especially since seemingly compatible Futures for the DOM have been proposed by WHATWG. http://dom.spec.whatwg.org/#futures

Now, my proposed async/await version of this would look like:

AsyncAwaitTest.ts
/// <reference path='q.module.d.ts' />

import q = module("q");

module AsyncTest {
    export class TestClass {

        constructor(public value: number = 0) { }

        private wait(): Promise {
            var def = q.defer();

            setTimeout(() => def.resolve(), 1000);

            return def.promise;
        }

        public async incrementValue(): Promise {
            await this.wait();
            this.value++;
        }

        private static async asyncMain() {
            var tc = new TestClass();
            var task = tc.incrementValue();

            console.log(tc.value);

            await task;

            console.log(tc.value);
        }

        public static main() {
            TestClass.asyncMain();
        }
    }
}

AsyncTest.TestClass.main();
I completely agree that this doesn't really reduce the complexity of Promises at all since it effectively just adds syntactic sugar on top of them, but since Async/Await are currently being investigated for the 1.x according to the roadmap, it would be awesome if the pattern was implemented to conform to an existing, accepted community standard. It would instantly make many existing libraries, like WinJS and jQuery, compatible with the pattern.
Apr 22, 2013 at 1:26 AM
What would the output JavaScript look like for this:
class Argh {
  constructor(private someService, private otherService, private yetAnotherService) { }

  public async complicated(things: string[]) {
    if (await someService.validate(things) && await otherService.verify(things)) {
      for (var i = 0; i < things.length; i++) {
        await yetAnotherService.save(things[i]);
      }
    } else {
      throw new Error("Bother.");
    }
  }
}
Apr 22, 2013 at 7:03 AM
Just so you know, I'm working on a detailed response that hashes out some of the details better, but it's worth noting those services would have to be "strongly" typed because await would necessarily only operate on those types that implement the Promise interface described up top.
Apr 22, 2013 at 10:16 AM
@AndrewGaspar

Fair point, and I agree that if they are looking at async/await it should comply with promises, it's just that "await" gives me completely different associations than being a "continuation", so I just think I in general don't like the keywords as they are...

And then I am like... It's kind of simple enough as is, do we really need anything on top of it.
Aug 25, 2013 at 6:37 AM