How to Work with Result Types
- written by Roland BOLE (Instructor)
This is an announcement for a new codeLab 01 that I have recently published on Github.
Blog image

👋 Happy to share my new codeLab!

Highlight img
Info

To run this codeLab please follow the instructions in the README file on Github.

What does this CodeLab contain?

In this codeLab we pick on aspects from our new course which we are currently developing. Here, we publish smaller excerpts from it.

The example is about a dictionary service which contains a Motoko backend service and a SvelteKit frontend application.

The frontend application can search for some entries in the dictionary and if there is a match the result will be displayed on the frontend.

Application

This is how the frontend looks like

This example focuses on Motoko’s Result variant type.

An example for a Result type could be:

type Result = {
 #ok;
 #err;
};

A value of this type may be one of two possible variants, namely #ok or #err. These variants are used to indicate either the successful result of a function or a possible error during the evaluation of a function.

In our example it means, if we find a word to display, the function returns an Ok type with the dictionary entry.

If the search does not find a corresponding word, then the function should return an Err type and an appropriate message can be displayed to the user.

Let us discuss the backend canister briefly

The backend canister contains two public functions: getWord is a query function which does not change the state of the canister and, addWord adds a word to the dictionary, thereby changing the status of the canister.

Both functions have a Result type for the return value which can be imported from the Motoko base library. The default result expands two types in this scenario: <Entry, Text>.

The first type is used in the success case (#ok) and the second type in the error case (#err).

If there is a match then the result type is equal #ok and the record is sent back to the JavaScript client in our case.

If there is no match the result type is equal #err and this is sent back to the client with a custom text to display.

Highlight img
Info

The switch expression helps to decide if a record is found or not. The switch expression is a powerful control flow construct that allows pattern matching on its input.

public query func getWord(word : Text) : async Result.Result <Entry, Text> {
    let entry = db.get(Text.toLowercase(word));
    switch entry {
      case (null) {
        return #err("Word not found");
      };
      case (?entry) {
        return #ok(entry);
      };
    }
  };
public func addWord(word : Text, description: Text) : async Result.Result <Text, Text> {
  db.put(word, { word = word; description = description; });
  #ok("Word added")
};
Highlight img
Exercise

Feel free to add the frontend application part for the addWord function and also an error (#err) Result type, if a new entry cannot be stored successfully in the data store by your own as an exceriece.

The canister stores the dictionary data in a HashMap.

Highlight img
Warning

Note that this store is not stable and you will lose the content if you update the backend canister.

var db = Map.HashMap<Text, Entry>(0, Text.equal, Text.hash);
Highlight img
Info

In general, a HashMap is also known as a hash table. It is a data structure that stores key-value pairs. It allows insertion, deletion, and lookup of elements based on their unique keys.

The data record going to be stored is defined in a type called Entry.

type Entry = {
  word: Text;
  description: Text;
};

Let us discuss the frontend canister

The relevant code is shown in the +page.svelte file.

Thanks to the automatic type creation these types can also be used for our TypeScript frontend.

import { backend } from '../stores/ic';
import type { Result} from '../declarations/backend/backend.did.d.ts';
import type { Entry} from '../declarations/backend/backend.did.d.ts';

let search:string = '', error:string ='', entry:Entry | null, result:Result;

const handleOnSubmit = async () => {
  try {

    error = ''; entry = null;

    result = await backend.getWord(search);
    if ('ok' in result) {
      entry = result.ok;
    }
    else {
      error = result.err;
    }
    
    search = '';
  } catch (err) {
      console.error('>> ',err);
  }
};

As you can see, a general import of the backend actor function is used. In this example it works as a wrapper for the automatically generated ready to use actor.

Furthermore, the type definitions for the Result and the Entry type are imported. Also, these types are generated from the dfx generate command which is executed by a dfx deploy command.

Rember, you can generate these types every time on your own with the command dfx generate.

The rest is pretty straightforward. You can check if there is an ok property in the Result object and, if so then there is a match and Sveltes reactivity can be used to display the data.

Below, there is the snippet for the template code.

<div class="box">
  <form on:submit|preventDefault={handleOnSubmit}>
    <label for="name">Search a word</label>
    <input id="search" alt="Search" type="text" bind:value={search}/>
    <button type="submit">Click Me!</button>
  </form> 
  <div>
    {#if entry}
      <b>{ entry.word }</b>
      <div>{ entry.description}</div>
    {:else }
      <div>{ error }</div>
    {/if}
  </div> 
</div>

Now you know a little bit more about the Internet Computer, Happy Coding! 🚀

Don’t forget to register to our newsletter and we will keep you updated about the course development progress.