Skip to content

Calling one async function from tail position in another escapes enclosing try blocks #791

Closed
@stereotype441

Description

@stereotype441

The analysis server's PreviewSite.handleGetRequest function attempts to catch exceptions occuring while handling a get request, so that an error response may be returned instead:

  Future<void> handleGetRequest(HttpRequest request) async {
    ...
    try {
      ...
      return respond(request, DartFilePage(this, unitInfo)); // (1)
      ...
    } catch (exception, stackTrace) { // (2)
      try {
        await respond(
            request,
            createExceptionPageWithPath(path, '$exception', stackTrace),
            HttpStatus.internalServerError);
      } catch (exception, stackTrace) {
        ...
      }
    }
  }

Where the respond method is itself asynchronous:

  Future<void> respond(HttpRequest request, Page page,
      [int code = HttpStatus.ok]) async {
    HttpResponse response = request.response;
    response.statusCode = code;
    ...
    response.write(await page.generate(request.uri.queryParameters));
    response.close();
  }

The intended behavior here is that when an HttpRequest is received, a DartFilePage object is created and passed to respond. Then respond calls its generate method to generate HTML text. If an exception occurs during page.generate, then it should be caught by the catch block at (2), and so the HttpRequest should be completed with an error response. But that doesn't happen. What happens instead that the HttpRequest is never completed, so the analysis server appears to hang.

After a lot of single stepping in the debugger and scratching my head, I've figured out the reason the code isn't behaving as intended: since there is no await keyword at (1), as soon as a Future is received from the call to respond, that future is returned from handleGetRequest, and then the caller waits for it to complete. If respond's call to page.generate, encounters an exception, that occurs after handleGetRequest has already exited, so its try/catch is no longer in scope, and the exception propagates to the caller.

I've been able to get the intended behavior by changing (1) to use return await rather than just return. This ensures that handleGetRequest actually waits for the future returned by respond to complete, so that if an exception occurs during page.generate, the try/catch will still be in scope, and the intended exception page will be generated.

I think there's a bit of a user trap here. Usually return await and return are equivalent (because of the "implicit await" behavior of return statements inside of async functions) , so I've gotten into the habit of changing one into the other without really thinking carefully. But when the return statement is inside of a try block, the behavior is very different, and shortening return await to just return is almost certainly a big mistake.

I don't really have a concrete proposal for what to do about this--perhaps the best answer is just to have a lint that detects this problematic pattern (cc @pq). But I figured I would start by reaching out to the language folks to see if you have any other ideas.

Metadata

Metadata

Assignees

No one assigned

    Labels

    requestRequests to resolve a particular developer problemstate-duplicateThis issue or pull request already exists

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions