Search code examples
javascriptgowebassembly

How to "throw" JS error from Go web assembly?


I tried

js.Global().Call("throw", "yeet")

but got back

panic: 1:1: expected operand, found 'type' [recovered] wasm_exec.f7bab17184626fa7f3ebe6c157d4026825842d39bfae444ef945e60ec7d3b0f1.js:51 panic: syscall/js: Value.Call: property throw is not a function, got undefined

I see there's an Error type defined in syscall/js, but there's nothing about throwing it https://golang.org/pkg/syscall/js/#Error


Solution

  • It's not possible to throw a JS error from WebAssembly. In JS, when you throw a value, the JS runtime unwinds the stack to the nearest try block, or logs an uncaught error. WASM execution is performed within an isolated sandbox in a separate execution environment that cannot directly access the JS stack. From the WASM docs:

    Each WebAssembly module executes within a sandboxed environment separated from the host runtime using fault isolation techniques.

    If WASM calls into JS code that throws, the error will be caught by the WASM runtime and handled as though the WASM code had panicked. WASM has access to traps, but those are intended to halt execution immediately at the runtime level (and aren't implemented in Go's syscall/js module).

    The idiomatic approach to representing code execution that may fail is to return a Promise, then either resolve that promise on success or reject it on failure. The calling JS code can await the promise execution within a try/catch block and handle the error there, or use promise chaining and handle errors in a .catch() callback. Here's a brief example:

    func main() {
        c := make(chan struct{})
    
        js.Global().Set("doSomething", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
            handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
                resolve := args[0]
                reject := args[1]
    
                go func() {
                    data, err := doSomeWork()
                    if err != nil {
                        // err should be an instance of `error`, eg `errors.New("some error")`
                        errorConstructor := js.Global().Get("Error")
                        errorObject := errorConstructor.New(err.Error())
                        reject.Invoke(errorObject)
                    } else {
                        resolve.Invoke(js.ValueOf(data))
                    }
                }()
    
                return nil
            })
    
            promiseConstructor := js.Global().Get("Promise")
            return promiseConstructor.New(handler)
        })
    
        <-c
    }
    

    Then, in your JS code:

    (async () => {
      try {
        await window.doSomething();
      } catch (err) {
        console.log('caught error from WASM:', err);
      }
    }();
    

    or

    window.doSomething()
      .then(_ => /* ... */)
      .catch(err => {
        console.log('caught error from WASM:', err);
      });