Skip to content
FutureBlitz

    Install

    Concurrency, 6 of 8

    Async and Await

    Write code that waits for files and other work without blocking, with Async Functions, Await, and the errors their Tasks carry.

    What you’ll build

    A function that reads a file without blocking, hands the counting to a worker thread, and gives a one-line summary. The program starts it for two files at once, one of which doesn’t exist, and handles the error that Task carries.

    An Async Function

    Async Function marks a function that can wait. Calling one runs it until its first Await, and then gives its Task<T>. Inside it, Await t waits for a Task: the function pauses there, the main thread goes on to other work, and the function resumes on the main thread when the Task is done.

    notes.txt
    one
    two
    three
    main.fb
    Import std.core
    Import std.io
    Import std.text
    
    Function CountVowels(text: String): Int
        Local count = 0
        For c In text.Chars
            If "aeiou".Contains(c.ToString()) Then count += 1
        End For
        Return count
    End Function
    
    Async Function Summarize(path: String): String Throws IOError
        Const text = Try Await File.ReadTextAsync(path)
        Local vowels = Await Spawn(Function() => CountVowels(text))
        Return "{path}: {text.SplitLines().Count} lines, {vowels} vowels"
    End Function
    
    Local first = Summarize("notes.txt")
    Local second = Summarize("missing.txt")
    Print("both started")
    
    Try
        Print(Await first)
        Print(Await second)
    Catch e As IOError
        Print("couldn't summarize: {e.Kind}")
    End Try
    Output
    both started
    notes.txt: 3 lines, 5 vowels
    couldn't summarize: NOT_FOUND

    Both calls start their reads before both started prints, so the two files are read at the same time. File.ReadTextAsync is std.io’s Async form of File.ReadText. Await Spawn(...) sends the counting to a worker thread and waits for it without blocking the main thread, so heavy work and waiting mix freely. text is a Const, so the work on the other thread can use it.

    Errors in a Task

    An Async Function’s errors go in its Task, so calling it never throws. Await throws them, so an Await of a Task that can fail needs Try, as a call that throws does. The Task’s type says what it can throw: Summarize gives a Task<String> Throws IOError.

    Import std.core
    Import std.io
    
    Async Function Load(path: String): String Throws IOError
        Return Try Await File.ReadTextAsync(path)
    End Function
    
    Local text = Await Load("notes.txt")
    Print(text)
    $ fb run main.fb
       Compiling main.fb
    main.fb:8:14: error: this Await can throw IOError, as its Task's Async Function can, so it needs Try, Try?, or a Try block
      |
    8 | Local text = Await Load("notes.txt")
      |              ^^^^^^^^^^^^^^^^^^^^^^^
    error: couldn't build main.fb because of 1 error

    In the program, the second Await throws the IOError that reading missing.txt gave, and the Catch handles it. Each Await of a Task that failed throws its error again.

    Where Await goes

    Await goes in Async Functions, Async Methods, and Async lambdas, and in top-level code. In a function that isn’t Async, it’s an error:

    Import std.core
    Import std.io
    
    Function Load(path: String): String Throws IOError
        Return Try Await File.ReadTextAsync(path)
    End Function
    
    Print("never built")
    $ fb run main.fb
       Compiling main.fb
    main.fb:5:16: error: Await goes in an Async Function or lambda, whose code can wait without blocking its thread, or in top-level code
      |
    5 |     Return Try Await File.ReadTextAsync(path)
      |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    error: couldn't build main.fb because of 1 error

    An Await in top-level code waits as Wait() does, and while it waits, the main thread runs the Async Functions that are ready to go on. Async code always runs on the main thread, so it shares the program’s Globals and objects without locks; only the work given to Spawn, Thread.Start, and Parallel For runs elsewhere.

    std’s I/O has Async forms throughout: files, sockets, processes, and HTTP, like HttpClient’s GetAsync. A Method can be Async too, and so can an Interface’s.

    What you learned

    • Calling an Async Function starts it and gives a Task<T>.
    • Await waits for a Task without blocking the main thread, and gives its result.
    • Await Spawn(...) runs heavy work on another core from Async code.
    • An Async Function’s errors go in its Task, and Await throws them, so it needs Try.
    • Async code runs on the main thread.

    Report an issue with this page or with FutureBlitz on GitHub.