Proposal Details
Let's say I have an async JavaScript function:
globalThis.myAsyncFunction = async () => 42;
Ideally I would be able to do one of these:
var n int
n = js.Global().Call("myAsyncFunction").Wait().Int()
n = js.Global().Call("myAsyncFunction").Await().Int()
n = js.Await(js.Global().Call("myAsyncFunction")).Int()
n = (<-js.Global().Call("myAsyncFunction").Chan()).Int()
Creating a promise in Go code for use by JavaScript code is ok-ish (it's not great): just use the new Promise(callback) constructor with a Go-defined js.Func callback.
var jsDoThingCallback = js.FuncOf(func(this Value, args []Value) interface{} {
r1 := <-myChannelFromSomewhere
r2, err := someFuncThatUsesChans(r1)
if err != nil {
args[1].Invoke(errorConstructor.New(err.Error()))
return nil
}
r3, err := someFuncThatSleeps(r2)
if err != nil {
args[1].Invoke(errorConstructor.New(err.Error()))
return nil
}
args[0].Invoke(r3)
return nil
})
func DoThingAsync() js.Value {
return promiseConstructor.New(jsDoThingCallback)
}
The other direction -- unwrapping a JavaScript Promise instance on the Go-side by waiting for it -- is worse.
// Ugh. I have to manage this intermediate channel myself and remember to handle some edge cases.
var n int
p := promiseConstructor.Call("resolve", js.Global().Call("myAsyncFunction"))
type result struct {
value js.Value
err error
}
ch := make(chan result, 1)
onFulfilled := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
ch <- result{args[0], nil}
close(ch)
return nil
})
defer onFulfilled.Release()
onRejected := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
ch <- result{js.Value{}, js.Error{args[0]}}
close(ch)
return nil
})
defer onRejected.Release()
p.Call("then", onFulfilled, onRejected)
r := <-ch
if r.err != nil {
panic(r.err)
}
n = r.value.Int()
// Would have to do *all that again* for another `await nextFunctionThatUsesResult(n)` 😭
Here's the algorithm for the ECMAScript 2025 Await( value ) abstract operation: https://tc39.es/ecma262/multipage/control-abstraction-objects.html#await
It seems like the .Wait() method convention is already in the standard library with sync.WaitGroup wg.Wait(). Here's an idea for how that might look in Go code. I'm not a Go channels wizard so this might be the completely wrong way to do this.
// Wait waits for the thenable v to fulfill or reject and returns the resulting value or error.
// This is equivalent to the await operator in JavaScript.
func (v js.Value) Wait() (js.Value, error) {
p := promiseConstructor.Call("resolve", v)
type result struct {
value js.Value
err error
}
ch := make(chan result, 1)
onFulfilled := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
ch <- result{args[0], nil}
close(ch)
return nil
})
defer onFulfilled.Release()
onRejected := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
ch <- result{js.Value{}, js.Error{args[0]}}
close(ch)
return nil
})
defer onRejected.Release()
p.Call("then", onFulfilled, onRejected)
r := <-ch
// Unsure if want to panic on error, or return the error.
// If a synchronous function throws an error it panics. Don't know whether to stick
// to that convention or to return a (js.Value, error) multivalue.
return r.value, r.err
// OR
if r.err == nil {
return r.value
} else {
panic(r.err)
}
}
There's already some of this promise stuff in the Go standard library
|
fetchPromise := js.Global().Call("fetch", req.URL.String(), opt) |
|
var ( |
|
respCh = make(chan *Response, 1) |
|
errCh = make(chan error, 1) |
|
success, failure js.Func |
|
) |
|
success = js.FuncOf(func(this js.Value, args []js.Value) any { |
|
success.Release() |
|
failure.Release() |
|
|
|
result := args[0] |
|
header := Header{} |
|
// https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries |
|
headersIt := result.Get("headers").Call("entries") |
|
for { |
|
n := headersIt.Call("next") |
|
if n.Get("done").Bool() { |
|
break |
|
} |
|
pair := n.Get("value") |
|
key, value := pair.Index(0).String(), pair.Index(1).String() |
|
ck := CanonicalHeaderKey(key) |
|
header[ck] = append(header[ck], value) |
|
} |
|
|
|
contentLength := int64(0) |
|
clHeader := header.Get("Content-Length") |
|
switch { |
|
case clHeader != "": |
|
cl, err := strconv.ParseInt(clHeader, 10, 64) |
|
if err != nil { |
|
errCh <- fmt.Errorf("net/http: ill-formed Content-Length header: %v", err) |
|
return nil |
|
} |
|
if cl < 0 { |
|
// Content-Length values less than 0 are invalid. |
|
// See: https://datatracker.ietf.org/doc/html/rfc2616/#section-14.13 |
|
errCh <- fmt.Errorf("net/http: invalid Content-Length header: %q", clHeader) |
|
return nil |
|
} |
|
contentLength = cl |
|
default: |
|
// If the response length is not declared, set it to -1. |
|
contentLength = -1 |
|
} |
|
|
|
b := result.Get("body") |
|
var body io.ReadCloser |
|
// The body is undefined when the browser does not support streaming response bodies (Firefox), |
|
// and null in certain error cases, i.e. when the request is blocked because of CORS settings. |
|
if !b.IsUndefined() && !b.IsNull() { |
|
body = &streamReader{stream: b.Call("getReader")} |
|
} else { |
|
// Fall back to using ArrayBuffer |
|
// https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer |
|
body = &arrayReader{arrayPromise: result.Call("arrayBuffer")} |
|
} |
|
|
|
code := result.Get("status").Int() |
|
|
|
uncompressed := false |
|
if ascii.EqualFold(header.Get("Content-Encoding"), "gzip") { |
|
// The fetch api will decode the gzip, but Content-Encoding not be deleted. |
|
header.Del("Content-Encoding") |
|
header.Del("Content-Length") |
|
contentLength = -1 |
|
uncompressed = true |
|
} |
|
|
|
respCh <- &Response{ |
|
Status: fmt.Sprintf("%d %s", code, StatusText(code)), |
|
StatusCode: code, |
|
Header: header, |
|
ContentLength: contentLength, |
|
Uncompressed: uncompressed, |
|
Body: body, |
|
Request: req, |
|
} |
|
|
|
return nil |
|
}) |
|
failure = js.FuncOf(func(this js.Value, args []js.Value) any { |
|
success.Release() |
|
failure.Release() |
|
|
|
err := args[0] |
|
// The error is a JS Error type |
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error |
|
// We can use the toString() method to get a string representation of the error. |
|
errMsg := err.Call("toString").String() |
|
// Errors can optionally contain a cause. |
|
if cause := err.Get("cause"); !cause.IsUndefined() { |
|
// The exact type of the cause is not defined, |
|
// but if it's another error, we can call toString() on it too. |
|
if !cause.Get("toString").IsUndefined() { |
|
errMsg += ": " + cause.Call("toString").String() |
|
} else if cause.Type() == js.TypeString { |
|
errMsg += ": " + cause.String() |
|
} |
|
} |
|
errCh <- fmt.Errorf("net/http: fetch() failed: %s", errMsg) |
|
return nil |
|
}) |
|
|
|
fetchPromise.Call("then", success, failure) |
|
select { |
|
case <-req.Context().Done(): |
|
if !ac.IsUndefined() { |
|
// Abort the Fetch request. |
|
ac.Call("abort") |
|
} |
|
return nil, req.Context().Err() |
|
case resp := <-respCh: |
|
return resp, nil |
|
case err := <-errCh: |
|
return nil, err |
|
} |
so it seems like this is a
thing that people need to do. I think that providing a
.Wait() or
Await(v) or something would be a good way to "one good way to do it"-ify this.
Proposal Details
Let's say I have an async JavaScript function:
Ideally I would be able to do one of these:
Creating a promise in Go code for use by JavaScript code is ok-ish (it's not great): just use the
new Promise(callback)constructor with a Go-definedjs.Funccallback.The other direction -- unwrapping a JavaScript
Promiseinstance on the Go-side by waiting for it -- is worse.Here's the algorithm for the ECMAScript 2025
Await( value )abstract operation: https://tc39.es/ecma262/multipage/control-abstraction-objects.html#awaitIt seems like the
.Wait()method convention is already in the standard library withsync.WaitGroupwg.Wait(). Here's an idea for how that might look in Go code. I'm not a Go channels wizard so this might be the completely wrong way to do this.There's already some of this promise stuff in the Go standard library
go/src/net/http/roundtrip_js.go
Lines 129 to 245 in 1d0f5c4
.Wait()orAwait(v)or something would be a good way to "one good way to do it"-ify this.