Error handling with TinyGo WASM
My approach to handling errors with WASM
Error handling “is the process comprised of anticipation, detection, and resolution of application errors, programming errors or communication errors.” In essence, it’s the process of designing procedures to interpret errors. Go developers handle their errors in different ways. One method I’ve seen is with function log.Fatal
. The function writes the error to the log and terminates the program with error code 1.
Another method would be to return the error. This is common with libraries, as they allow users to implement it to determine how to handle the error. I was writing a WASM application with TinyGo recently, and I needed an efficient way to handle errors. In this post, I’ll detail my approach to implementing error handling.
TinyGo WASM Setup
To start, I made an empty directory. I switched my terminal’s active directory to said directory. I ran the following command to copy a JavaScript file. This file is required to help the browser make sense of the WASM binary:
cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js wasm_exec.js
I proceeded by adding a file named index.html
to the directory. The file's contents will contain the minimum code required to load the WASM dependencies. The content of the file is as follows:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Go wasm</title>
</head> <body>
<script src="wasm_exec.js"></script>
</body>
</html>
I’ll add a script tag to index.html
, this tag will be responsible for loading my WASM binary and running it. The script tag will have the following code:
<script>if (!WebAssembly.instantiateStreaming) {
// polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
} const go = new Go(); let inst; WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(
async (result) => {
inst = result.instance;
go.run(inst);
}
);</script>
The script tag will download the binary and execute it. The resulting instance is assigned to variable inst
. inst
will house exported functions. Next, I’ll start writing my Go code. The last step will be to add a file called main.go
to the new directory. The file will have the following contents:
package mainfunc main(){}
To compile my Go code into WASM, I ran the following command:
tinygo build -o lib.wasm -target wasm ./main.go
To access index.html
, I spun up a file server and served the directory.
My Basic Function
I’ll start by defining a function I want TinyGo to export to the browser. I’ll add the function to my main.go
file. The function is represented by the following code:
// Must add the following comment for it
// to work.
//export Add
func Add(x, y int) int { return x + y
}
It is important to have the last comment in your function to be export $function_name
, with function_name
being your function’s name. This will export your function to the browser. The function can be accessed with the following JavaScript code:
inst.exports.Add
Now, to shoehorn some things in (s/o to Robert Weber), let’s say this function’s x parameter must be greater than 1. And if x is not, it should return an error. The error should also be compatible with JavaScript’s error handling mechanism, i.e., try, catch. Returning errors to JavaScript with TinyGo WASM is not so simple.
Upon experimenting with TinyGo, I’ve noticed that when I return a string from a Go function to JavaScript, it shows up as a number on the browser. Since strings had this behavior, I did not bother trying to have the function return an error object. My first idea was to see if log.Fatal
could be a good way to throw an exception on the browser.
The Process of Determining a Proper Method To Handle Errors
I’ll update my function Add
to check if x is less than 2. If so, log.Fatal
will be invoked. Here is the resulting Add
function:
// Must add the following comment for it
// to work.
//export Add
func Add(x, y int) int { if x < 2 {
log.Fatal("x must be greater than 1.")
} return x + y
}
It is time to test if the code above will throw an exception.

The code does generate an exception. However, the exception is a by-product of invoking log.Fatal
. I can’t capture the error message as it is logged to the console. Another approach would be to declare a JavaScript function to throw the exception. In theory, a Go function and the JavaScript functions it invokes will run on the same thread. So throwing an exception should halt execution and return a proper error message.
I’ll start by importing a JavaScript function into my Go program. I’ll do this by updating index.html
. I’ll add the following JavaScript code after initializing constant Go
:
go.importObject.env = {
...go.importObject.env ,
'main.throw': (e) => {
console.log(e)
throw e
}
}
As you can see, the defined JavaScript function will throw an error on invocation. To get this to work fully, I must define a body-less function in my main.go
file. To do so, I’ll add the following code to the file:
func throw(e string)
This will allow my Go program to access and execute the function. I’ll update function Add
to use this function to throw an error. The resulting function is as follows:
// Must add the following comment for it
// to work.
//export Add
func Add(x, y int) int { if x < 2 { err := "X must be greater than 1!"
throw(err)
} return x + y
}
Here is the code in action:

As you can see, invoking throw
from function Add
is working. However, an integer is being passed as the parameter. Again, this is because TinyGo only communicates in numbers with the browser. And the number passed is a memory address. Get ready, folks, I’m about to get my hands dirty digging around memory.
Accessing Tiny Memory
First, I’ll need to update my Go code. I’ll need a function to take a string and assert it as a byte
array. Once asserted, I need to pass the address of the first byte
to my JavaScript function, as well as the length of the string. The Go function to do this will be defined as follows:
func throwError(e string, l int){
firstByte := &(([]byte)(e)[0])
throw(firstByte,l)
}
The first line will assert the string as a byte, and then return a pointer to the first index of the byte
array. I’ll update throw to take another parameter. The parameter will be called l
. It is the length of the error string. Here is the updated definition of function throw
, on the Go side:
func throw(e *byte, l int)
I’ll update Add
to use throwError
instead of throw
. This is what the function will look like after the change:
// Must add the following comment for it
// to work.
//export Add
func Add(x, y int) int { if x < 2 {
err := "X must be greater than 1!"
throwError(err, len(err))
} return x + y
}
I’ll proceed by updating the JavaScript definition of the function in index.html
. The new JavaScript definition of function throw will be as follows :
go.importObject.env = {
...go.importObject.env ,
'main.throw': (e, length) => {
console.log(e)
throw getString(e, length)
},
}
getString
will be the function responsible for getting the string from memory and decoding it to a UTF-8 string. The following code will perform this operation :
function getString(addr, length){
const memory = inst.exports.memory; const extractedBuffer = new Uint8Array(memory.buffer, addr, length); const str = new TextDecoder("utf8").decode(extractedBuffer);
return str
}
Constant memory
represents my program’s memory. extractedBuffer
will be a sub-array of bytes extracted from the program’s memory buffer. addr
will specify the start address of the bytes to extract. length
will determine how many bytes will be extracted after the start address, which should match the length of the error message. I’ll use TextDecoder to convert the bytes to a string and return the result. Here is the result of this combination:

Finally, my program throws a string, which I can display as an error message.
Conclusion
TinyGo has an impressive WASM implementation. I can’t get over how easy it is to share functions between Go and the browser. The only limitation is it only communicates with the browser using numbers. This can be corrected by accessing memory, as seen in the example above.
Throwing an exception from a Go function will result in the function’s execution halting. It will also prevent silent errors and eliminate the need to check returned function data for any additional errors. Lastly, I feel that error handling with WASM libraries should be, to an extent, browser compatible. Compatible in the sense that other JavaScript components can reliably handle the library’s errors.