WebAssembly: DOM Access and Error Handling

Welcome to tutorial no. 2 of our WebAssembly tutorial series.

Series Index

Introduction to WebAssembly Using Go
Accessing DOM from Go using Javascript

In the first tutorial of this tutorial series, we created and exposed a function from Go and called it using JavaScript. I highly recommend reading the first part https://golangbot.com/webassembly-using-go/ if you have not read it yet.

In this tutorial, we will develop a UI for our application, handle errors, and also manipulate the DOM of the browser from Go.

Creating the UI and calling the wasm function

Let's create a very simple UI using HTML. It will contain a text area to get the input JSON, a submit button to format the input JSON, and another text area to display the output.

Let's modify the existing ~/Documents/webassembly/assets/index.html in the assets folder to include the UI.

<html>  
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>
        <script>
            const go = new Go();
            WebAssembly.instantiateStreaming(fetch("json.wasm"), go.importObject).then((result) => {
                go.run(result.instance);
            });
        </script>
    </head>
    <body>
         <textarea id="jsoninput" name="jsoninput" cols="80" rows="20"></textarea>
         <input id="button" type="submit" name="button" value="pretty json" onclick="json(jsoninput.value)"/>
         <textarea id="jsonoutput" name="jsonoutput" cols="80" rows="20"></textarea>
    </body>
    <script>
        var json = function(input) {
            jsonoutput.value = formatJSON(input)
        }
     </script>
</html>  

In line no. 13 of the above HTML, we create a text area with id jsoninput. This will be our text area where we input the JSON to be formatted.

Next, we create a submit button and when the button is clicked, the json JavaScript function in line no. 18 will be called. This function takes the input JSON as a parameter, calls the formatJSON wasm function which we created in the previous tutorial and sets the output to the jsonoutput text area defined in line no. 15.

Let's compile and run this program and see if it works.

cd ~/Documents/webassembly/cmd/wasm/  
GOOS=js GOARCH=wasm go build -o  ../../assets/json.wasm  
cd ~/Documents/webassembly/cmd/server/  
go run main.go  

Go to the browser and type localhost:9090. You can see the UI with two text areas and a button.

Input the following text in the first text area.

{"website":"golangbot.com", "tutorials": {"string":"https://golangbot.com/strings/", "maps":"https://golangbot.com/maps/", "goroutine":"https://golangbot.com/goroutines/", "channels":"https://golangbot.com/channels/"}}

Now tap on the pretty json button. You can see that the JSON is formatted and printed in the output text area.

call Go function from JavaScript You can see the above output in the browser. We have successfully called the wasm function and formatted the JSON.

Accessing the DOM from Go using JavaScript

In the above section, we called the wasm function, got the formatted JSON string output, and set the output text area with the formatted JSON using JavaScript.

There is one more way to achieve the same output. Instead of passing the formatted JSON string to javascript, it is possible to access the browser's DOM from Go and set the formatted JSON string to the output text area.

Let's see how this is done.

We need to modify the jsonWrapper function in ~/Documents/webassembly/cmd/wasm/main.go to achieve this.

func jsonWrapper() js.Func {  
    jsonfunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        if len(args) != 1 {
            return "Invalid no of arguments passed"
        }
        jsDoc := js.Global().Get("document")
        if !jsDoc.Truthy() {
            return "Unable to get document object"
        }
        jsonOuputTextArea := jsDoc.Call("getElementById", "jsonoutput")
        if !jsonOuputTextArea.Truthy() {
            return "Unable to get output text area"
        }
        inputJSON := args[0].String()
        fmt.Printf("input %s\n", inputJSON)
        pretty, err := prettyJson(inputJSON)
        if err != nil {
            errStr := fmt.Sprintf("unable to parse JSON. Error %s occurred\n", err)
            return errStr
        }
        jsonOuputTextArea.Set("value", pretty)
        return nil
    })

    return jsonfunc
}

In line no. 6, we try to get the document property of JavaScript from the global scope. This property is needed to access the output JSON text area. The Truthy function in line no. 7 is JavaScript's way of testing for nil. If truthy returns false, it means the property doesn't exist. Hence the appropriate error string is returned to JavaScript. We do not explicitly return a Go error type. The reason for this and how to handle errors is covered in the next section.

In line no. 10, we use the call method to call the getElementById function on the jsDoc JavaScript object and pass it the jsonoutput argument. In JavaScript, this line of code corresponds to,

jsDoc.getElementById("jsonoutput")  

If you recollect, jsonoutput is the id of the output text area in index.html.

This will return the reference to the jsonoutput text area. As we did earlier, we check for truthy.

Now we have access to the jsonoutput text area. In line no. 21, we use the set method to set the value property of the jsonoutput text area to the formatted JSON string. This will display the formatted JSON in the output text area.

The changes to the Go side of the program is done.

A minor change is needed in the ~/Documents/webassembly/assets/index.html. Since the JSON is set from Go directly by manipulating the browser's DOM rather than JavaScript, we can remove the following piece of code.

Change line no. 19 from

jsonoutput.value = formatJSON(input)  

to

var result = formatJSON(input)  
console.log("Value returned from Go", result)  

We have removed the code that sets jsonoutput value from JavaScript since this is done from the Go side. We just log the result to the console. If there is an error in the JSON input, the error string that was returned from jsonfunc will be logged to the console.
Do note that the output text area will not be cleared if there is an error. It will still keep showing its existing content. This will be fixed in the next section.

Try running the program again using the following commands and then opening localhost:9090 in the browser.

cd ~/Documents/webassembly/cmd/wasm/  
GOOS=js GOARCH=wasm go build -o  ../../assets/json.wasm  
cd ~/Documents/webassembly/cmd/server/  
go run main.go  

The output will be the same. If a Valid JSON is passed, it will be formatted and printed. This is now done from the Go code by manipulating the DOM rather than from the JavaScript. If you pass an invalid JSON, the corresponding error will be logged to the console.



Error Handling

In the previous section, we just returned a string from the jsonfunc function when an error occurred during the JSON formatting.

The idiomatic way of handling errors in Go is to return the error. Let's modify the jsonWrapper function in ~/Documents/webassembly/cmd/wasm/main.go to return an error and see what happens.

func jsonWrapper() js.Func {  
    jsonfunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        if len(args) != 1  {
            return errors.New("Invalid no of arguments passed")
        }
        jsDoc := js.Global().Get("document")
        if !jsDoc.Truthy() {
            return errors.New("Unable to get document object")
        }
        jsonOuputTextArea := jsDoc.Call("getElementById", "jsonoutput")
        if !jsonOuputTextArea.Truthy() {
            return errors.New("Unable to get output text area")
        }
        inputJSON := args[0].String()
        fmt.Printf("input %s\n", inputJSON)
        pretty, err := prettyJson(inputJSON)
        if err != nil {
            errStr := fmt.Sprintf("unable to parse JSON. Error %s occurred\n", err)
            return errors.New(errStr)
        }
        jsonOuputTextArea.Set("value", pretty)
        return nil
    })
    return jsonfunc
}

Line no. 4 is changed to return an error instead of a string. Similar changes are done in other places where error needs to be returned.

Compile and run the code and try inputting an incorrect JSON and see what happens. I have provided the invalid JSON string dfs333{"website as input.

panic: ValueOf: invalid value wasm_exec.js

The program has crashed with the following stack trace.

input dfs333{"website wasm_exec.js:47:14  
panic: ValueOf: invalid value wasm_exec.js:47:14  
<empty string> wasm_exec.js:47:14  
goroutine 6 [running]: wasm_exec.js:47:14  
syscall/js.ValueOf(0x1db00, 0x40e390, 0x6, 0x7ff8000100000017) wasm_exec.js:47:14  
    /usr/local/go/src/syscall/js/js.go:219 +0x13f wasm_exec.js:47:14
syscall/js.Value.Set(0x7ff8000100000012, 0x41a0d0, 0x3b31e, 0x6, 0x1db00, 0x40e390) wasm_exec.js:47:14  
    /usr/local/go/src/syscall/js/js.go:314 +0x7 wasm_exec.js:47:14
syscall/js.handleEvent() wasm_exec.js:47:14  
    /usr/local/go/src/syscall/js/func.go:91 +0x25 wasm_exec.js:47:14
exit code: 2 wasm_exec.js:138:14  
Value returned from Go undefined  

As we already discussed in the last tutorial, any value returned by jsonfunc will automatically be mapped to the corresponding JavaScript value using the ValueOf function. If you take a quick look at the documentation of this function, you can see that there is no mapping for Go's error type to a corresponding JavaScript type. This is the reason the program is crashing with error panic: ValueOf: invalid value when an error type is returned from Go. There is no way to pass errors from Go to Javascript currently. This feature could be added in the future, but currently, it's not available. We have to look at other options when returning errors.

One way to do this is to establish a contract between Go and JavaScript. For example, we can return a map from Go to JavaScript. If the map contains an error key, it can be considered as an error by JavaScript and handled appropriately.

Let's modify the jsonWrapper function to do this.

func jsonWrapper() js.Func {  
    jsonfunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        if len(args) != 1 {
            result := map[string]interface{}{
                "error": "Invalid no of arguments passed",
            }
            return result
        }
        jsDoc := js.Global().Get("document")
        if !jsDoc.Truthy() {
            result := map[string]interface{}{
                "error": "Unable to get document object",
            }
            return result
        }
        jsonOuputTextArea := jsDoc.Call("getElementById", "jsonoutput")
        if !jsonOuputTextArea.Truthy() {
            result := map[string]interface{}{
                "error": "Unable to get output text area",
            }
            return result
        }
        inputJSON := args[0].String()
        fmt.Printf("input %s\n", inputJSON)
        pretty, err := prettyJson(inputJSON)
        if err != nil {
            errStr := fmt.Sprintf("unable to parse JSON. Error %s occurred\n", err)
            result := map[string]interface{}{
                "error": errStr,
            }
            return result
        }
        jsonOuputTextArea.Set("value", pretty)
        return nil
    })
    return jsonfunc
}

In the above snippet, in line no. 4, a map named result with an error key is created and returned with the corresponding error. Similar changes are done in other places. The JavaScript side can now check for the existence of this key. If this key is present, it means an error has occurred and it can be handled appropriately.

The modified index.html file is provided below. Changes are done only to the JavaScript section starting at line no. 17.

...
    <script>
         var json = function(input) {
                var result = formatJSON(input)
                if (( result != null) && ('error' in result)) {
                    console.log("Go return value", result)
                    jsonoutput.value = ""
                    alert(result.error)
                }
        }
    </script>
</html>  

The return value from Go is first validated for null and then it is checked to find whether the error key is present. If the error key is present, it means some error has occurred when processing the JSON. The output text area is first cleared and then a popup alert is shown to the user with the error message.

Compile and run the program again. Try passing an invalid JSON. You can see an alert with the error message. The output text area is also cleared.

Go WebAssembly error handling

This brings us to the end of this tutorial.

The source code is available at https://github.com/golangbot/webassembly/tree/tutorial2

Please leave your comments and feedback.

If you would like to advertise on this website, hire me, or if you have any other development requirements please email to naveen[at]golangbot[dot]com.

Naveen Ramanathan

Naveen Ramanathan is a software engineer with interests in Go, Docker, Kubernetes, Swift, Python, and Web Assembly. If you would like to hire him, please mail to naveen[at]golangbot[dot]com.