Async JS with Promises from Go
In JavaScript, Promise’s are the foundation of async/await.
Lets take up an example, consider the below code, This will create a Promise that resolves with a message after 3 seconds:
const p = new Promise((resolve, reject) => { setTimeout(() => { resolve("A happy hippo hopped and hiccupped") }, 3000) })
async function, the await on the Promise above, so after 3 seconds you receive the message:
// This is an async function, which can contain "await" statements inside async function MyFunc() { // Create the Promise const p = new Promise((resolve, reject) => { // After a 3 second timeout, this calls "resolve" with the message we're passing setTimeout(() => { resolve("A happy hippo hopped and hiccupped") }, 3000) }) // Await for the Promise - this resolves after 3 seconds const message = await p console.log(message) }
As per the documentation, we cannot make blocking calls in Go inside a function that is invoked by JavaScript directly — if we do that, we will get into a deadlock and the app will crash. Here is what the documentation recommends, all blocking calls be inside a goroutine, which raises the problem of then returning the value to the JavaScript code.
Quote from Documentation.
Invoking the wrapped Go function from JavaScript will pause the event loop and spawn a new goroutine. Other wrapped functions which are triggered during a call from Go to JavaScript get executed on the same goroutine.
As a consequence, if one wrapped function blocks, JavaScript’s event loop is blocked until that function returns. Hence, calling any async JavaScript API, which requires the event loop, like fetch (http.Client), will cause an immediate deadlock. Therefore a blocking function should explicitly start a new goroutine.
Using a Promise is probably the best way to solve this problem: avoiding deadlocks whereas permitting programming with idiomatical JavaScript.We saw within the previous article that we are able to produce custom JavaScript objects from Go, and this is applicable to promises as well.We just need to create the Promise object by passing a function to the constructor. in a Pure-JS code , this function has two arguments, which are functions themselves, resolve should be invoked with the final result when the promise is completed. and when reject can be called when there is a an error to make the promise fail.
Here’s an updated MyGoFunc that resolves with a message after 3 seconds:
// MyGoFunc returns a Promise that resolves after 3 seconds with a message func MyGoFunc() js.Func { return js.FuncOf(func(this js.Value, args []js.Value) interface{} { // Handler for the Promise: this is a JS function // It receives two arguments, which are JS functions themselves: resolve and reject handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { resolve := args[0] // Commented out because this Promise never fails //reject := args[1] // Now that we have a way to return the response to JS, spawn a goroutine // This way, we don't block the event loop and avoid a deadlock go func() { // Block the goroutine for 3 seconds time.Sleep(3 * time.Second) // Resolve the Promise, passing anything back to JavaScript // This is done by invoking the "resolve" function passed to the handler resolve.Invoke("A happy hippo hopped and hiccupped") }() // The handler of a Promise doesn't return any value return nil }) // Create and return the Promise object promiseConstructor := js.Global().Get("Promise") return promiseConstructor.New(handler) })
Let's invoke this from a JavaScript function
async function MyFunc() { // Get the Promise from Go const p = MyGoFunc() // Show the current UNIX timestamps (in seconds) console.log(Math.floor(Date.now() / 1000)) // Await for the Promise to resolve const message = await p // Show the current timestamp in seconds, then the result of the Promise console.log(Math.floor(Date.now() / 1000), message) } /* Result: 1604114580 1604114580 "A happy hippo hopped and hiccupped" */
If your Go code errors, you can throw exceptions to JavaScript by using the reject function instead. For example:
// MyGoFunc returns a Promise that fails with an exception about 50% of times func MyGoFunc() js.Func { return js.FuncOf(func(this js.Value, args []js.Value) interface{} { // Handler for the Promise handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { resolve := args[0] reject := args[1] // Run this code asynchronously go func() { // Cause a failure 50% of times if rand.Int()%2 == 0 { // Invoke the resolve function passing a plain JS object/dictionary resolve.Invoke(map[string]interface{}{ "message": "it worked!", "error": nil, }) } else { // Assume this were a Go error object err := errors.New("it failed") // Create a JS Error object and pass it to the reject function // The constructor for Error accepts a string, // so we need to get the error message as string from "err" errorConstructor := js.Global().Get("Error") errorObject := errorConstructor.New(err.Error()) reject.Invoke(errorObject) } }() // The handler of a Promise doesn't return any value return nil }) // Create and return the Promise object promiseConstructor := js.Global().Get("Promise") return promiseConstructor.New(handler) }) }
While we invoke this from JavaScript, we can see the returned object about half of the times, and we get an exception the other half. Note that we’re invoking the reject function with an actual JavaScript Error object, as best practice in JavaScript!
async function MyFunc() { try { console.log(await MyGoFunc()) } catch (err) { console.error('Caught exception', err) } } /* Result is either: {error: null, message: "it worked!"} Or a caught exception (followed by the stack trace): Caught exception Error: Nope, it failed */