Search code examples
javascriptgowebassemblylanguage-interoperability

How to wait a js async function from golang wasm?


I have written a little function await in order to handle async javascript function from go:

func await(awaitable js.Value) (ret js.Value, ok bool) {
    if awaitable.Type() != js.TypeObject || awaitable.Get("then").Type() != js.TypeFunction {
        return awaitable, true
    }
    done := make(chan struct{})


    onResolve := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        glg.Info("resolve")
        ret = args[0]
        ok = true
        close(done)
        return nil
    })
    defer onResolve.Release()

    onReject := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        glg.Info("reject")
        ret = args[0]
        ok = false
        close(done)
        return nil
    })
    defer onReject.Release()

    onCatch := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        glg.Info("catch")
        ret = args[0]
        ok = false
        close(done)
        return nil
    })
    defer onCatch.Release()


    glg.Info("before await")
    awaitable.Call("then", onResolve, onReject).Call("catch", onCatch)
    // i also tried go func() {awaitable.Call("then", onResolve, onReject).Call("catch", onCatch)}()
    glg.Info("after await")
    <-done
    glg.Info("I never reach the end")
    return
}

The problem is that when I call the function with or without goroutine, the event handler seems blocked and I'm forced to reload the page. I never get into any callback and my channel is never closed. Is there any idiomatic way to call await on a promise from Golang in wasm ?


Solution

  • You don't need goroutines for that one :D

    I see a few problems in this code that doesn't make it idiomatic and can lead to some errors (some of which might lock your callback and lead to this situation you're describing):

    • You shouldn't close the done channel inside the handling function. This can lead to unwanted closing due to concurrencies and it's a bad practice in general.
    • Since there's no guarantee in the order of execution, changing external variables might lead to some concurrency problems. It's best to communicate with the external functions using channels only.
    • The results from onResolve and onCatch should use separate channels. This can lead to better handling of the outputs and sort out the results for the main function individually, ideally through a select statement.
    • There's no need for separate onReject and onCatch methods, since they are overlapping each other's responsibilities.

    If I had to design this await function, I'd simplify it a bit to something like this:

    func await(awaitable js.Value) ([]js.Value, []js.Value) {
        then := make(chan []js.Value)
        defer close(then)
        thenFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
            then <- args
            return nil
        })
        defer thenFunc.Release()
    
        catch := make(chan []js.Value)
        defer close(catch)
        catchFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
            catch <- args
            return nil
        })
        defer catchFunc.Release()
    
        awaitable.Call("then", thenFunc).Call("catch", catchFunc)
    
        select {
        case result := <-then:
            return result, nil
        case err := <-catch:
            return nil, err
        }
    }
    

    This would make the function idiomatic, as the function would return resolve, reject data, kinda like a result, err situation that is common in Go. Also a bit easier to deal with unwanted concurrencies since we don't handle variables in different closures.

    Last but not least, make sure you're not calling both resolve and reject in your Javascript Promise, as the Release method in js.Func explicitly tells that you should not access such resources once they are released:

    // Release frees up resources allocated for the function.
    // The function must not be invoked after calling Release.
    // It is allowed to call Release while the function is still running.