diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 45d74c73c8adf26124e714b57af034c7d4660ec8..e0e137071fd5b9ba0310fc236264dd39c4dd637e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,8 +31,8 @@ build:
     - go mod vendor -v
     - mkdir -p release
     - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -o release/xxdk.wasm main.go
-    - GOOS=js GOARCH=wasm go build -mod vendor -ldflags '-w -s' -trimpath -o release/xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/...
-    - GOOS=js GOARCH=wasm go build -mod vendor -ldflags '-w -s' -trimpath -o release/xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/...
+    - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/...
+    - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/...
     - cp wasm_exec.js release/
     - cp indexedDb/impl/channels/channelsIndexedDbWorker.js release/
     - cp indexedDb/impl/dm/dmIndexedDbWorker.js release/
diff --git a/indexedDb/impl/channels/main.go b/indexedDb/impl/channels/main.go
index 932b20799141570c24bc8567127505e4700760e9..73a34dd4c96f2d54339dba7863c95f6b01a18dfe 100644
--- a/indexedDb/impl/channels/main.go
+++ b/indexedDb/impl/channels/main.go
@@ -32,7 +32,7 @@ func main() {
 	js.Global().Set("LogToFile", js.FuncOf(wasm.LogToFile))
 	js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter))
 
-	m := &manager{mh: worker.NewThreadManager("ChannelsIndexedDbWorker")}
+	m := &manager{mh: worker.NewThreadManager("ChannelsIndexedDbWorker", true)}
 	m.registerCallbacks()
 	m.mh.SignalReady()
 	<-make(chan bool)
diff --git a/indexedDb/impl/dm/main.go b/indexedDb/impl/dm/main.go
index c58bbef7783099bea66daba24a4b0628ac80ac87..3dac36d4c62505043305e657181ae1a6104c1c16 100644
--- a/indexedDb/impl/dm/main.go
+++ b/indexedDb/impl/dm/main.go
@@ -32,7 +32,7 @@ func main() {
 	js.Global().Set("LogToFile", js.FuncOf(wasm.LogToFile))
 	js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter))
 
-	m := &manager{mh: worker.NewThreadManager("DmIndexedDbWorker")}
+	m := &manager{mh: worker.NewThreadManager("DmIndexedDbWorker", true)}
 	m.registerCallbacks()
 	m.mh.SignalReady()
 	<-make(chan bool)
diff --git a/indexedDb/worker/channels/init.go b/indexedDb/worker/channels/init.go
index 2645fb98b65693e712e5d44276e7e7531d125471..04b7a8f4392ac78051bd9cce001be45a98477dca 100644
--- a/indexedDb/worker/channels/init.go
+++ b/indexedDb/worker/channels/init.go
@@ -66,7 +66,7 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
 	deletedMessageCB DeletedMessageCallback, mutedUserCB MutedUserCallback) (
 	channels.EventModel, error) {
 
-	wm, err := worker.NewManager(wasmJsPath, "channelsIndexedDb")
+	wm, err := worker.NewManager(wasmJsPath, "channelsIndexedDb", true)
 	if err != nil {
 		return nil, err
 	}
diff --git a/indexedDb/worker/dm/init.go b/indexedDb/worker/dm/init.go
index f5a1ab08737af6db1fb2fa7dcc1801e756bcbe1a..145edf935ba9804dd315c2196d301bb60581ecfb 100644
--- a/indexedDb/worker/dm/init.go
+++ b/indexedDb/worker/dm/init.go
@@ -41,7 +41,7 @@ type NewWASMEventModelMessage struct {
 func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher,
 	cb MessageReceivedCallback) (dm.EventModel, error) {
 
-	wh, err := worker.NewManager(wasmJsPath, "dmIndexedDb")
+	wh, err := worker.NewManager(wasmJsPath, "dmIndexedDb", true)
 	if err != nil {
 		return nil, err
 	}
diff --git a/main.go b/main.go
index 80fea11e65a139a0d939c1c05a6ada764bd92ab1..6a25f8d5da5dc386ecd2a1ecc31acd49220f3ada 100644
--- a/main.go
+++ b/main.go
@@ -15,7 +15,6 @@ import (
 	"syscall/js"
 
 	jww "github.com/spf13/jwalterweatherman"
-	"gitlab.com/elixxir/client/v4/bindings"
 	"gitlab.com/elixxir/xxdk-wasm/storage"
 	"gitlab.com/elixxir/xxdk-wasm/utils"
 	"gitlab.com/elixxir/xxdk-wasm/wasm"
@@ -37,7 +36,6 @@ func init() {
 
 func main() {
 	fmt.Println("Starting xxDK WebAssembly bindings.")
-	fmt.Printf("Client version %s\n", bindings.GetVersion())
 
 	// storage/password.go
 	js.Global().Set("GetOrInitPassword", js.FuncOf(storage.GetOrInitPassword))
diff --git a/utils/convert.go b/utils/convert.go
index a5c79e309529089ebb918fc77f701f7a633c8d05..b1f2cd10172bca7e88ff6c77931c754ab1b7f1d8 100644
--- a/utils/convert.go
+++ b/utils/convert.go
@@ -49,3 +49,14 @@ func JsonToJS(inputJson []byte) (js.Value, error) {
 
 	return js.ValueOf(jsObj), nil
 }
+
+// JsErrorToJson converts the Javascript error to JSON. This should be used for
+// all Javascript error objects instead of JsonToJS.
+func JsErrorToJson(value js.Value) string {
+	if value.IsUndefined() {
+		return "null"
+	}
+
+	properties := Object.Call("getOwnPropertyNames", value)
+	return JSON.Call("stringify", value, properties).String()
+}
diff --git a/utils/convert_test.go b/utils/convert_test.go
index 74bd9ca6c68e5e95639f413f2da0ab58b5faed31..508c27f783f1365f28b4b7b78b7389fc4d3ff1a6 100644
--- a/utils/convert_test.go
+++ b/utils/convert_test.go
@@ -241,3 +241,65 @@ func TestJsonToJSJsToJson(t *testing.T) {
 			"\nexpected: %s\nreceived: %s", jsonData, jsJson)
 	}
 }
+
+// Tests that JsErrorToJson can convert a Javascript object to JSON that matches
+// the output of json.Marshal on the Go version of the same object.
+func TestJsErrorToJson(t *testing.T) {
+	testObj := map[string]any{
+		"nil":    nil,
+		"bool":   true,
+		"int":    1,
+		"float":  1.5,
+		"string": "I am string",
+		"array":  []any{1, 2, 3},
+		"object": map[string]any{"int": 5},
+	}
+
+	expected, err := json.Marshal(testObj)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsJson := JsErrorToJson(js.ValueOf(testObj))
+
+	// Javascript does not return the JSON object fields sorted so the letters
+	// of each Javascript string are sorted and compared
+	er := []rune(string(expected))
+	sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] })
+	jj := []rune(jsJson)
+	sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] })
+
+	if string(er) != string(jj) {
+		t.Errorf("Recieved incorrect JSON from Javascript object."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
+
+// Tests that JsErrorToJson return a null object when the Javascript object is
+// undefined.
+func TestJsErrorToJson_Undefined(t *testing.T) {
+	expected, err := json.Marshal(nil)
+	if err != nil {
+		t.Errorf("Failed to JSON marshal test object: %+v", err)
+	}
+
+	jsJson := JsErrorToJson(js.Undefined())
+
+	if string(expected) != jsJson {
+		t.Errorf("Recieved incorrect JSON from Javascript object."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
+
+// Tests that JsErrorToJson returns a JSON object containing the original error
+// string.
+func TestJsErrorToJson_ErrorObject(t *testing.T) {
+	expected := "An error"
+	jsErr := Error.New(expected)
+	jsJson := JsErrorToJson(jsErr)
+
+	if !strings.Contains(jsJson, expected) {
+		t.Errorf("Recieved incorrect JSON from Javascript error."+
+			"\nexpected: %s\nreceived: %s", expected, jsJson)
+	}
+}
diff --git a/worker/manager.go b/worker/manager.go
index 9a0ac78af8e0d2822a773bebc951c96a706a7017..1a28ac2f99e803b902cdac1e1696ec031d42ca48 100644
--- a/worker/manager.go
+++ b/worker/manager.go
@@ -19,9 +19,6 @@ import (
 	"time"
 )
 
-// TODO:
-//  1. Add tests for manager.go and thread.go
-
 // initID is the ID for the first item in the callback list. If the list only
 // contains one callback, then this is the ID of that callback. If the list has
 // autogenerated unique IDs, this is the initial ID to start at.
@@ -31,11 +28,11 @@ const initID = uint64(0)
 const (
 	// workerInitialConnectionTimeout is the time to wait to receive initial
 	// contact from a new worker before timing out.
-	workerInitialConnectionTimeout = 16 * time.Second
+	workerInitialConnectionTimeout = 90 * time.Second
 
 	// ResponseTimeout is the general time to wait after sending a message to
 	// receive a response before timing out.
-	ResponseTimeout = 8 * time.Second
+	ResponseTimeout = 30 * time.Second
 )
 
 // ReceptionCallback is the function that handles incoming data from the worker.
@@ -62,20 +59,25 @@ type Manager struct {
 	// name describes the worker. It is used for debugging and logging purposes.
 	name string
 
+	// messageLogging determines if debug message logs should be printed every
+	// time a message is sent/received to/from the worker.
+	messageLogging bool
+
 	mux sync.Mutex
 }
 
 // NewManager generates a new Manager. This functions will only return once
 // communication with the worker has been established.
-func NewManager(aURL, name string) (*Manager, error) {
+func NewManager(aURL, name string, messageLogging bool) (*Manager, error) {
 	// Create new worker options with the given name
 	opts := newWorkerOptions("", "", name)
 
 	m := &Manager{
-		worker:      js.Global().Get("Worker").New(aURL, opts),
-		callbacks:   make(map[Tag]map[uint64]ReceptionCallback),
-		responseIDs: make(map[Tag]uint64),
-		name:        name,
+		worker:         js.Global().Get("Worker").New(aURL, opts),
+		callbacks:      make(map[Tag]map[uint64]ReceptionCallback),
+		responseIDs:    make(map[Tag]uint64),
+		name:           name,
+		messageLogging: messageLogging,
 	}
 
 	// Register listeners on the Javascript worker object that receive messages
@@ -109,10 +111,12 @@ func (m *Manager) SendMessage(
 		id = m.registerReplyCallback(tag, receptionCB)
 	}
 
-	jww.DEBUG.Printf("[WW] [%s] Main sending message for %q and ID %d with "+
-		"data: %s", m.name, tag, id, data)
+	if m.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Main sending message for %q and ID %d "+
+			"with data: %s", m.name, tag, id, data)
+	}
 
-	msg := message{
+	msg := Message{
 		Tag:  tag,
 		ID:   id,
 		Data: data,
@@ -129,13 +133,16 @@ func (m *Manager) SendMessage(
 // receiveMessage is registered with the Javascript event listener and is called
 // every time a new message from the worker is received.
 func (m *Manager) receiveMessage(data []byte) error {
-	var msg message
+	var msg Message
 	err := json.Unmarshal(data, &msg)
 	if err != nil {
 		return err
 	}
-	jww.DEBUG.Printf("[WW] [%s] Main received message for %q and ID %d with "+
-		"data: %s", m.name, msg.Tag, msg.ID, msg.Data)
+
+	if m.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Main received message for %q and ID %d "+
+			"with data: %s", m.name, msg.Tag, msg.ID, msg.Data)
+	}
 
 	callback, err := m.getCallback(msg.Tag, msg.ID, msg.DeleteCB)
 	if err != nil {
@@ -222,6 +229,14 @@ func (m *Manager) getNextID(tag Tag) uint64 {
 	return id
 }
 
+// GetWorker returns the web worker object. This returned so the worker object
+// can be returned to the Javascript layer for it to communicate with the worker
+// thread.
+func (m *Manager) GetWorker() js.Value { return m.worker }
+
+// Name returns the name of the web worker object.
+func (m *Manager) Name() string { return m.name }
+
 ////////////////////////////////////////////////////////////////////////////////
 // Javascript Call Wrappers                                                   //
 ////////////////////////////////////////////////////////////////////////////////
@@ -242,20 +257,31 @@ func (m *Manager) addEventListeners() {
 		return nil
 	})
 
+	// Create listener for when an error event is fired on the worker. This
+	// occurs when an error occurs in the worker.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event
+	errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
+		event := args[0]
+		jww.ERROR.Printf("[WW] [%s] Main received error event: %s",
+			m.name, utils.JsErrorToJson(event))
+		return nil
+	})
+
 	// Create listener for when a messageerror event is fired on the worker.
 	// This occurs when it receives a message that cannot be deserialized.
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event
-	messageError := js.FuncOf(func(_ js.Value, args []js.Value) any {
+	messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
 		event := args[0]
-		jww.ERROR.Printf("[WW] [%s] Main received error message from worker: %s",
-			m.name, utils.JsToJson(event))
+		jww.ERROR.Printf("[WW] [%s] Main received message error event: %s",
+			m.name, utils.JsErrorToJson(event))
 		return nil
 	})
 
 	// Register each event listener on the worker using addEventListener
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
 	m.worker.Call("addEventListener", "message", messageEvent)
-	m.worker.Call("addEventListener", "messageerror", messageError)
+	m.worker.Call("addEventListener", "error", errorEvent)
+	m.worker.Call("addEventListener", "messageerror", messageerrorEvent)
 }
 
 // postMessage sends a message to the worker.
@@ -290,7 +316,7 @@ func (m *Manager) Terminate() {
 // Each property is optional; leave a property empty to use the defaults (as
 // documented). The available properties are:
 //   - type - The type of worker to create. The value can be either "classic" or
-//     "module". If not specified, the default used is classic.
+//     "module". If not specified, the default used is "classic".
 //   - credentials - The type of credentials to use for the worker. The value
 //     can be "omit", "same-origin", or "include". If it is not specified, or if
 //     the type is "classic", then the default used is "omit" (no credentials
diff --git a/worker/manager_test.go b/worker/manager_test.go
index 3c7c1a357af95723d00cbc9b9a8011b39d52742c..2eadaff2edeaf66a4071f387f608a602c6967b8d 100644
--- a/worker/manager_test.go
+++ b/worker/manager_test.go
@@ -10,22 +10,147 @@
 package worker
 
 import (
+	"encoding/json"
+	"reflect"
 	"testing"
+	"time"
 )
 
-func TestNewManager(t *testing.T) {
-}
+// Tests Manager.receiveMessage calls the expected callback.
+func TestManager_receiveMessage(t *testing.T) {
+	m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)}
+
+	msg := Message{Tag: readyTag, ID: 5}
+	cbChan := make(chan struct{})
+	cb := func([]byte) { cbChan <- struct{}{} }
+	m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
 
-func TestManager_SendMessage(t *testing.T) {
+	err = m.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
 }
 
-func TestManager_receiveMessage(t *testing.T) {
+// Tests Manager.getCallback returns the expected callback and deletes only the
+// given callback when deleteCB is true.
+func TestManager_getCallback(t *testing.T) {
+	m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)}
+
+	// Add new callback and check that it is returned by getCallback
+	tag, id1 := readyTag, uint64(5)
+	cb := func([]byte) {}
+	m.callbacks[tag] = map[uint64]ReceptionCallback{id1: cb}
+
+	received, err := m.getCallback(tag, id1, false)
+	if err != nil {
+		t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err)
+	}
+
+	if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() {
+		t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received)
+	}
+
+	// Add new callback under the same tag but with deleteCB set to true and
+	// check that it is returned by getCallback and that it was deleted from the
+	// map while id1 was not
+	id2 := uint64(56)
+	cb = func([]byte) {}
+	m.callbacks[tag][id2] = cb
+
+	received, err = m.getCallback(tag, id2, true)
+	if err != nil {
+		t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id2, err)
+	}
+
+	if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() {
+		t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received)
+	}
+
+	received, err = m.getCallback(tag, id1, false)
+	if err != nil {
+		t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err)
+	}
+
+	received, err = m.getCallback(tag, id2, true)
+	if err == nil {
+		t.Errorf("getCallback did not get error when trying to get deleted "+
+			"callback for tag %q and ID %d", tag, id2)
+	}
 }
 
-func TestManager_getHandler(t *testing.T) {
+// Tests that Manager.RegisterCallback registers a callback that is then called
+// by Manager.receiveMessage.
+func TestManager_RegisterCallback(t *testing.T) {
+	m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)}
+
+	msg := Message{Tag: readyTag, ID: initID}
+	cbChan := make(chan struct{})
+	cb := func([]byte) { cbChan <- struct{}{} }
+	m.RegisterCallback(msg.Tag, cb)
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = m.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
 }
 
-func TestManager_RegisterHandler(t *testing.T) {
+// Tests that Manager.registerReplyCallback registers a callback that is then
+// called by Manager.receiveMessage.
+func TestManager_registerReplyCallback(t *testing.T) {
+	m := &Manager{
+		callbacks:   make(map[Tag]map[uint64]ReceptionCallback),
+		responseIDs: make(map[Tag]uint64),
+	}
+
+	msg := Message{Tag: readyTag, ID: 5}
+	cbChan := make(chan struct{})
+	cb := func([]byte) { cbChan <- struct{}{} }
+	m.registerReplyCallback(msg.Tag, cb)
+	m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb}
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = m.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
 }
 
 // Tests that Manager.getNextID returns the expected ID for various Tags.
@@ -56,11 +181,32 @@ func TestManager_getNextID(t *testing.T) {
 // Javascript Call Wrappers                                                   //
 ////////////////////////////////////////////////////////////////////////////////
 
-func TestManager_addEventListeners(t *testing.T) {
-}
+// Tests that newWorkerOptions returns a Javascript object with the expected
+// type, credentials, and name fields.
+func Test_newWorkerOptions(t *testing.T) {
+	for i, workerType := range []string{"classic", "module"} {
+		for j, credentials := range []string{"omit", "same-origin", "include"} {
+			for k, name := range []string{"name1", "name2", "name3"} {
+				opts := newWorkerOptions(workerType, credentials, name)
 
-func TestManager_postMessage(t *testing.T) {
-}
+				v := opts.Get("type").String()
+				if v != workerType {
+					t.Errorf("Unexpected type (%d, %d, %d)."+
+						"\nexpected: %s\nreceived: %s", i, j, k, workerType, v)
+				}
 
-func Test_newWorkerOptions(t *testing.T) {
+				v = opts.Get("credentials").String()
+				if v != credentials {
+					t.Errorf("Unexpected credentials (%d, %d, %d)."+
+						"\nexpected: %s\nreceived: %s", i, j, k, credentials, v)
+				}
+
+				v = opts.Get("name").String()
+				if v != name {
+					t.Errorf("Unexpected name (%d, %d, %d)."+
+						"\nexpected: %s\nreceived: %s", i, j, k, name, v)
+				}
+			}
+		}
+	}
 }
diff --git a/worker/message.go b/worker/message.go
index 133cb9b5fabe32f29bbda09bc6301b714c627224..3d3bc23f131ef96cdc97a3e8684d1386b07cbeb2 100644
--- a/worker/message.go
+++ b/worker/message.go
@@ -9,9 +9,9 @@
 
 package worker
 
-// message is the outer message that contains the contents of each message sent
+// Message is the outer message that contains the contents of each message sent
 // to the worker. It is transmitted as JSON.
-type message struct {
+type Message struct {
 	Tag      Tag    `json:"tag"`
 	ID       uint64 `json:"id"`
 	DeleteCB bool   `json:"deleteCB"`
diff --git a/worker/thread.go b/worker/thread.go
index c7d7aa974e0d69b10983bfea3e920c09fa8c2f6b..df824d045f4337c6384eac389f692b15bec200a8 100644
--- a/worker/thread.go
+++ b/worker/thread.go
@@ -35,15 +35,20 @@ type ThreadManager struct {
 	// name describes the worker. It is used for debugging and logging purposes.
 	name string
 
+	// messageLogging determines if debug message logs should be printed every
+	// time a message is sent/received to/from the worker.
+	messageLogging bool
+
 	mux sync.Mutex
 }
 
 // NewThreadManager initialises a new ThreadManager.
-func NewThreadManager(name string) *ThreadManager {
+func NewThreadManager(name string, messageLogging bool) *ThreadManager {
 	mh := &ThreadManager{
-		messages:  make(chan js.Value, 100),
-		callbacks: make(map[Tag]ThreadReceptionCallback),
-		name:      name,
+		messages:       make(chan js.Value, 100),
+		callbacks:      make(map[Tag]ThreadReceptionCallback),
+		name:           name,
+		messageLogging: messageLogging,
 	}
 
 	mh.addEventListeners()
@@ -60,14 +65,17 @@ func (tm *ThreadManager) SignalReady() {
 
 // SendMessage sends a message to the main thread for the given tag.
 func (tm *ThreadManager) SendMessage(tag Tag, data []byte) {
-	msg := message{
+	msg := Message{
 		Tag:      tag,
 		ID:       initID,
 		DeleteCB: false,
 		Data:     data,
 	}
-	jww.DEBUG.Printf("[WW] [%s] Worker sending message for %q with data: %s",
-		tm.name, tag, data)
+
+	if tm.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Worker sending message for %q with data: %s",
+			tm.name, tag, data)
+	}
 
 	payload, err := json.Marshal(msg)
 	if err != nil {
@@ -81,14 +89,17 @@ func (tm *ThreadManager) SendMessage(tag Tag, data []byte) {
 // sendResponse sends a reply to the main thread with the given tag and ID.
 func (tm *ThreadManager) sendResponse(
 	tag Tag, id uint64, data []byte) {
-	msg := message{
+	msg := Message{
 		Tag:      tag,
 		ID:       id,
 		DeleteCB: true,
 		Data:     data,
 	}
-	jww.DEBUG.Printf("[WW] [%s] Worker sending reply for %q and ID %d with "+
-		"data: %s", tm.name, tag, id, data)
+
+	if tm.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Worker sending reply for %q and ID %d "+
+			"with data: %s", tm.name, tag, id, data)
+	}
 
 	payload, err := json.Marshal(msg)
 	if err != nil {
@@ -103,13 +114,16 @@ func (tm *ThreadManager) sendResponse(
 // everytime a message from the main thread is received. If the registered
 // callback returns a response, it is sent to the main thread.
 func (tm *ThreadManager) receiveMessage(data []byte) error {
-	var msg message
+	var msg Message
 	err := json.Unmarshal(data, &msg)
 	if err != nil {
 		return err
 	}
-	jww.DEBUG.Printf("[WW] [%s] Worker received message for %q and ID %d with "+
-		"data: %s", tm.name, msg.Tag, msg.ID, msg.Data)
+
+	if tm.messageLogging {
+		jww.DEBUG.Printf("[WW] [%s] Worker received message for %q and ID %d "+
+			"with data: %s", tm.name, msg.Tag, msg.ID, msg.Data)
+	}
 
 	tm.mux.Lock()
 	callback, exists := tm.callbacks[msg.Tag]
@@ -167,20 +181,31 @@ func (tm *ThreadManager) addEventListeners() {
 		return nil
 	})
 
+	// Create listener for when an error event is fired on the worker. This
+	// occurs when an error occurs in the worker.
+	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event
+	errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
+		event := args[0]
+		jww.ERROR.Printf("[WW] [%s] Worker received error event: %s",
+			tm.name, utils.JsErrorToJson(event))
+		return nil
+	})
+
 	// Create listener for when a messageerror event is fired on the worker.
 	// This occurs when it receives a message that cannot be deserialized.
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event
-	messageError := js.FuncOf(func(_ js.Value, args []js.Value) any {
+	messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any {
 		event := args[0]
-		jww.ERROR.Printf("[WW] [%s] Worker received error message from main "+
-			"thread: %s", tm.name, utils.JsToJson(event))
+		jww.ERROR.Printf("[WW] [%s] Worker received message error event: %s",
+			tm.name, utils.JsErrorToJson(event))
 		return nil
 	})
 
 	// Register each event listener on the worker using addEventListener
 	// Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
 	js.Global().Call("addEventListener", "message", messageEvent)
-	js.Global().Call("addEventListener", "messageerror", messageError)
+	js.Global().Call("addEventListener", "error", errorEvent)
+	js.Global().Call("addEventListener", "messageerror", messageerrorEvent)
 }
 
 // postMessage sends a message from this worker to the main WASM thread.
diff --git a/worker/thread_test.go b/worker/thread_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8f7d09cd931f5e28002ea9cb22b764df834c8062
--- /dev/null
+++ b/worker/thread_test.go
@@ -0,0 +1,73 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2022 xx foundation                                             //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file.                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package worker
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+)
+
+// Tests that ThreadManager.receiveMessage calls the expected callback.
+func TestThreadManager_receiveMessage(t *testing.T) {
+	tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)}
+
+	msg := Message{Tag: readyTag, ID: 5}
+	cbChan := make(chan struct{})
+	cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil }
+	tm.callbacks[msg.Tag] = cb
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = tm.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}
+
+// Tests that ThreadManager.RegisterCallback registers a callback that is then
+// called by ThreadManager.receiveMessage.
+func TestThreadManager_RegisterCallback(t *testing.T) {
+	tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)}
+
+	msg := Message{Tag: readyTag, ID: 5}
+	cbChan := make(chan struct{})
+	cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil }
+	tm.RegisterCallback(msg.Tag, cb)
+
+	data, err := json.Marshal(msg)
+	if err != nil {
+		t.Fatalf("Failed to JSON marshal Message: %+v", err)
+	}
+
+	go func() {
+		select {
+		case <-cbChan:
+		case <-time.After(10 * time.Millisecond):
+			t.Error("Timed out waiting for callback to be called.")
+		}
+	}()
+
+	err = tm.receiveMessage(data)
+	if err != nil {
+		t.Errorf("Failed to receive message: %+v", err)
+	}
+}