what's the problem here with async?

I have this OnClick handler that's working:

    procedure ListTables_btnClick(Sender: TObject);    {$IFDEF PAS2JS} async; {$ENDIF}
. . .
procedure TMain_form.ListTables_btnClick(Sender: TObject);
var
  HttpRequest: TWebHttpRequest;
  req: TJSXMLHttpRequest;
  URL: string;

begin
  HttpRequest := TWebHttpRequest.Create(self);

  try
    URL := 'https://api.stellards.io/v1/schema/table?project='+PROJECT_ID;
    HttpRequest.URL := URL;
    HttpRequest.Command := httpGET;

    HttpRequest.Headers.AddPair('Accept', 'application/json');
    HttpRequest.Headers.AddPair('Content-Type', 'application/json');
    HttpRequest.Headers.AddPair('Authorization', ACCESS_TOKEN);

    req := TAwait.ExecP<TJSXMLHttpRequest>(HttpRequest.Perform());

    Text_memo.Lines.Text := TJSJSON.stringify(TJSJSON.parse(string(req.response)), nil, 2);

  finally
    HttpRequest.Free;
  end;
end;

When I try to extract out the non-UI logic into a separate function that returns the string data, it won't compile. It keeps telling me this when pointing to the line that's calling then new GetSchemaTable() function. If I put the call in an await call, it doesn't matter. I cannot find any combination of things that lets it compile. What the heck is the problem?


[Error] frmMain.pas(199): Incompatible types: got "TJSPromise" expected "String"


    procedure ListTables_btnClick(Sender: TObject);   
  private
    function GetSchemaTable() : string;   {$IFDEF PAS2JS} async; {$ENDIF}
    . . .
end;

procedure TMain_form.ListTables_btnClick(Sender: TObject);
begin
  Text_memo.Lines.Text := GetSchemaTable(); // <-- this is the error line
end;

function TMain_form.GetSchemaTable() : string;
var
  HttpRequest: TWebHttpRequest;
  req: TJSXMLHttpRequest;
  URL: string;
begin
  Result := '';

  HttpRequest := TWebHttpRequest.Create(self);

  try
    URL := 'https://api.stellards.io/v1/schema/table?project='+PROJECT_ID;
    HttpRequest.URL := URL;
    HttpRequest.Command := httpGET;

    HttpRequest.Headers.AddPair('Accept', 'application/json');
    HttpRequest.Headers.AddPair('Content-Type', 'application/json');
    HttpRequest.Headers.AddPair('Authorization', ACCESS_TOKEN);

    req := TAwait.ExecP<TJSXMLHttpRequest>(HttpRequest.Perform());

    Result := TJSJSON.stringify(TJSJSON.parse(string(req.response)), nil, 2);

  finally
    HttpRequest.Free;
  end;
end;

Hi,

Unfortunately async methods are not meant to be used this way. You separated your code into GetSchemaTable and made it async - nothing wrong with that so far, that is the only way you can do this. However, this means when the code is being transpiled into JavaScript, it will be turned into an async JavaScript function. By definition all async JavaScript functions are promises (= TJSPromise), it doesn't matter that you gave it a different return value in Pascal: async function - JavaScript | MDN

See some more discussion around this here: Must [sync] methods be always "procedures"? - #2 by Tunde

The only option you have is to make GetSchemaTable return a TJSPromise that you resolve with a string, and await the promise in ListTables_btnClick (marked as async too like you did before), something similar to:

function TMain_form.GetSchemaTable(): TJSPromise;
begin
  Result := TJSPromise.new(procedure (Resolve, Reject: TJSPromiseResolver) {$IFDEF PAS2jS}async{$ENDIF}
  var
    HttpRequest: TWebHttpRequest;
    url: string;
    req: TJSXMLHttpRequest;
  begin
    HttpRequest := TWebHttpRequest.Create(self);

    try
      URL := 'https://api.stellards.io/v1/schema/table?project='+PROJECT_ID;
      HttpRequest.URL := URL;
      HttpRequest.Command := httpGET;

      HttpRequest.Headers.AddPair('Accept', 'application/json');
      HttpRequest.Headers.AddPair('Content-Type', 'application/json');
      HttpRequest.Headers.AddPair('Authorization', ACCESS_TOKEN);

      req := TAwait.ExecP<TJSXMLHttpRequest>(HttpRequest.Perform());

      Resolve(TJSJSON.stringify(TJSJSON.parse(string(req.response)), nil, 2));
    finally
      HttpRequest.Free;
    end;
  end);
end;

procedure TMain_form.ListTables_btnClick(Sender: TObject);
begin
  Text_memo.Lines.Text := TAwait.ExecP<string>(GetSchemaTable());
end;

Ok, that makes sense.

What if I had something like this ... would that work? Or does the entire chain of calls need to be set up as async methods?

procedure TMain_form.GetSchemaTable( out aResult : string );

IOW, will the caller stay blocked until the procedure returns with a value in aResult? Or will it have to explicitly wait on that as well?

It won't stayed blocked, it will need to be a chain of async methods.

procedure TForm1.Button1Click(Sender: TObject);
var
  s: string;
begin
  s := '';
  WebMemo1.Lines.Add('Before async');
  MyAsyncMethod(s);
  WebMemo1.Lines.Add(s);
  WebMemo1.Lines.Add('After async');

  //Output will be:
  //Before async
  //(Empty line)
  //After async
end;

The only place you can await is inside an async method. Async methods however will execute separately from the context you called them from.

1 Like

Actually, I don't think I've seen the use of stuff like this before:

function TMain_form.GetSchemaTable(): TJSPromise;
begin
  Result := TJSPromise.new(procedure (Resolve, Reject: TJSPromiseResolver) {$IFDEF PAS2jS}async{$ENDIF}
  var
    HttpRequest: TWebHttpRequest;
 . . .
  begin
     try
. . .
        Resolve(TJSJSON.stringify(TJSJSON.parse(string(req.response)), nil, 2));
     finally
        HttpRequest.Free;
     end;
  end);
end;

The way I did it seems to have worked, with the OnClick method declared as async as well and using await to call GetSchemaTable, and having GetSchemaTable declared as async and return a string. Is that ok? (At least, I think that worked... I've tried a bunch of variations)

I want to have several of these access methods talking to the DB. It just seems like a lot of hoops to jump through. If I want to issue several requests in a row to the same URL, what's the simplest way to do it?

A function you want to await for should return a promise.
If you have a function returning a promise, you can await multiple calls to this promise function after each other.