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

Viewed 719

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

2 Answers

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);
  });

I disagree with @superhawk610 on that

It's not possible to throw a JS error from WebAssembly

Well you can throw it, but how practical it is? Depending on browser you may even get some readable stack trace, but most of cases having some Promise logic like @superhawk610 explained would make much more sense. However if you really wan't to throw exception here is simple example.

example

1. You can provide throw function stub

// Throw function stub to throw javascript exceptions
// Without func body!
func Throw(exception string, message string)

2. Then provide hint for assembler by creating yourpkg_js.s file

// Throw enables to throw javascript exceptions
TEXT ·Throw(SB), NOSPLIT, $0
  CallImport
  RET

3. Add js callback by extending wasm_exec / your wasm importObject

this.importObject = {
  go: {
    // ...

    // func Throw(exception string, message string)
    '<your-pkg-import-path>.Throw': (sp) => {
      const exception = loadString(sp + 8)
      const message = loadString(sp + 24)
      const throwable = globalThis[exception](message)
      throw throwable
    }
  }
}

Then you can throw errors by providing Error class name and message. e.g.

func main () {
  Throw("TypeError", "invalid arguments")
}
Related