Skip to content

proposal: syscall/js: method or function to await a js.Value #69720

Open
@jcbhmr

Description

@jcbhmr

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions