Skip to main content

Error handling

Using Option or Result is the preferred way of signaling errors in Motoko. They work in both synchronous and asynchronous contexts and make your APIs safer to use by encouraging clients to consider the error cases as well as the success cases. Exceptions should only be used to signal unexpected error states.

In addition to explicit error handling, Motoko provides traps and assertions for dealing with execution errors.

Error reporting with Option types

When a function might either return a value of type A or signal an error, it can return an option type ?A. In this pattern, null is used to indicate an error or missing result, while ?value wraps a successful outcome.

In the following example, if the markDone function sometimes fails and returns a number of seconds on success, its return type would be async ?Seconds. This makes it clear to callers that the result may be absent and must be handled safely.

Function definition:

  public shared func markDoneOption(id : TodoId) : async ?Seconds {
switch (todos.get(id)) {
case (?(#todo(todo))) {
let now = Time.now();
todos.put(id, #done(now));
?(secondsBetween(todo.opened, now))
};
case _ { null };
}
};

Function callsite:

  public shared func doneTodo2(id : Todo.TodoId) : async Text {
switch (await Todo.markDoneOption(id)) {
case null {
"Something went wrong."
};
case (?seconds) {
"Congrats! That took " # Int.toText(seconds) # " seconds."
};
};
};

The main drawback of using option types to signal errors is that all failures are represented by a single, non-descriptive null value. This means important information about why something failed is lost. As a result, the only message the program can show the user might be something vague like "Something went wrong."

For this reason, option types should only be used for errors when there's just one clear reason for failure, and it can be easily understood at the callsite.

Error reporting with Result types

While options are a built-in type, the Result is defined as a variant type like so:

type Result<Ok, Err> = { #ok : Ok; #err : Err }

Unlike option types, the Result type includes a second type parameter Err which allows you to specify exactly what kind of error occurred. This makes error handling more informative and flexible.

  public type TodoError = { #notFound; #alreadyDone : Time };

The previous example can be revised to use Result types:

Function definition:

  public shared func markDoneResult(id : TodoId) : async Result.Result<Seconds, TodoError> {
switch (todos.get(id)) {
case (?(#todo(todo))) {
let now = Time.now();
todos.put(id, #done(now));
#ok(secondsBetween(todo.opened, now))
};
case (?(#done(time))) {
#err(#alreadyDone(time))
};
case null {
#err(#notFound)
};
}
};

Function callsite:

  public shared func doneTodo3(id : Todo.TodoId) : async Text {
switch (await Todo.markDoneResult(id)) {
case (#err(#notFound)) {
"There is no Todo with that ID."
};
case (#err(#alreadyDone(at))) {
let doneAgo = secondsBetween(at, Time.now());
"You've already completed this todo " # Int.toText(doneAgo) # " seconds ago."
};
case (#ok(seconds)) {
"Congrats! That took " # Int.toText(seconds) # " seconds."
};
};
};

Pattern matching

The most common way of working with Option and Result is to use pattern matching. If you have a value of type ?Text, you can use the switch keyword to access the potential Text contents:

func greetOptional(optionalName : ?Text) : Text {
switch (optionalName) {
case (null) { "No name to be found." };
case (?name) { "Hello, " # name # "!" };
}
};
assert(greetOptional(?"Dominic") == "Hello, Dominic!");
assert(greetOptional(null) == "No name to be found");

Motoko does not let you access the optional value without also considering the case that it is missing.

With a Result type, you can use pattern matching to handle both success and error cases. Unlike option types, the #err case carries detailed information about what went wrong, not just a null value.

func greetResult(resultName : Result<Text, Text>) : Text {
switch (resultName) {
case (#err(error)) { "No name: " # error };
case (#ok(name)) { "Hello, " # name };
}
};
assert(greetResult(#ok("Dominic")) == "Hello, Dominic!");
assert(greetResult(#err("404 Not Found")) == "No name: 404 Not Found");

Sometimes you need to convert between Option and Result types. For example, a HashMap lookup returns null on failure (an Option), but if the caller has more context, they can turn that failure into a meaningful Result with an error message. On the other hand, sometimes you don’t need the extra detail from a Result and just want to convert any error (#err) into null.

The base library provides fromOption and toOption functions in the Result module that make converting between these two types easy.

Error reporting with Error (asynchronous errors)

Another way to handle errors in Motoko is with asynchronous Error handling, which is a limited form of exception handling. These errors can only be thrown and caught in asynchronous contexts, like inside shared functions or async blocks. Regular (non-shared) functions can’t use this kind of structured error handling.

This means you can throw an Error to exit a shared function early, and callers can try to run that code and catch the error if it happens. However, you can only use throw and catch inside asynchronous contexts.

Asynchronous Errors are best reserved for unexpected failures that you don’t expect most callers to handle. If you want the caller to handle possible failures explicitly, it’s better to use Result in your function’s return type.

Here’s how the markDone function might look using exceptions:

Function definition:

  public shared func markDoneException(id : TodoId) : async Seconds {
switch (todos.get(id)) {
case (?(#todo(todo))) {
let now = Time.now();
todos.put(id, #done(now));
secondsBetween(todo.opened, now)
};
case (?(#done _)) {
throw Error.reject("Already done")
};
case null {
throw Error.reject("Not Found")
};
}
};

Function callsite:

  public shared func doneTodo4(id : Todo.TodoId) : async Text {
try {
let seconds = await Todo.markDoneException(id);
"Congrats! That took " # Int.toText(seconds) # " seconds.";
} catch _ {
"Something went wrong.";
}
};

Traps

Traps immediately stop execution and roll back state. They are used for fatal errors that cannot be recovered.

import Debug "mo:base/Debug";

func divide(a : Nat, b : Nat) : Nat {
if (b == 0) {
Debug.trap("Cannot divide by zero");
};
return a / b;
};

Assertions

Assertions enforce expected conditions. If the condition is false, they introduce a trap but are not errors themselves.

func validateAge(age : Nat) : () {
assert(age >= 18); // Traps if age is below 18
};

How not to handle errors

Using sentinel values to report errors is generally a bad practice and strongly discouraged. For example, you might have markDone return -1 to indicate failure. In that case, the caller has to remember to check for that special value every time. It’s easy to forget, which can cause errors to go unnoticed or be detected too late.

Error code reference

Error CodeDescriptionExampleExplanation
M0001Parsing errors.let x = 5 + ;Syntax error - missing operand after the + operator.
M0003Module tried to import itself.import Self "self.mo"A module cannot import itself, creating a circular dependency.
M0009File not found for import.import Lib "wrong/path_to_file.mo"The specified file path in the import statement doesn't exist.
M0010Imported package was not defined.import { undefined_module } from "mo:package"The imported module doesn't exist in the specified package.
M0014Non-static expression in library or module.module { let x = Random.blob(); }Modules can only contain static definitions, not runtime expressions.
M0029Unbound type.func transform(x : UnknownType) { ... }The type UnknownType is referenced but hasn't been defined.
M0030Type field does not exist in type.type Person = { name : Text }; func getName(p : Person) : Nat { p.age }The field age doesn't exist in the Person type.
M0031Shared function has non-shared parameter type.public shared func process(obj : { var count : Nat }) { ... }Shared functions cannot have mutable types as parameters.
M0032Shared function has non-shared return type.public shared func getData() : { var data : [Nat] } { ... }Shared functions cannot return mutable types.
M0033Async has non-shared content type.async { var counter = 0 }Async blocks cannot contain mutable state.
M0036Invalid return type for shared query function.public shared query func getUsers() : async [User] { ... }Query functions cannot return async types.
M0038Misplaced await.func compute() { let x = await promise; }await can only be used in an async function or block.
M0045Wrong number of type arguments.func process<T>(x : Array<T, Nat>) { ... }Array type expects one type parameter, not two.
M0047Send capability required.actor { public func send() { IC.call(...); } }Functions calling IC management require explicit capability declarations.
M0050Literal does not have expected type.let x : Text = 42;Cannot assign a number literal to a variable of type Text.
M0055Cannot infer type of forward variable.func process() { x := 10; var x = 0; }Variable x is used before its declaration.
M0057Unbound variable.func calculate() { return result; }The variable result is used but hasn't been defined.
M0060Operator is not defined for operand types.let result = "text" - 5;The subtraction operator isn't defined for Text and Nat types.
M0064Misplaced '!' without enclosing do blockfunc test() { someFunction!(); }The ! operator must be used within a do block.
M0070Expected object type.let name = person.name; (where person is not an object)Trying to access a field on a non-object type.
M0072Field does not exist in type.type User = { id : Nat }; func getName(u : User) { u.name }The field name doesn't exist in the User type.
M0073Expected mutable assignment target.let x = 5; x := 10;Cannot reassign to an immutable variable declared with let.
M0082Expected iterable type.for (item in 42) { ... }The value 42 is not an iterable type for a for-loop.
M0088Expected async type.let result = await 42;Cannot await on a non-async value.
M0089Redundant ignore.ignore (5);The ignore is unnecessary for a value that doesn't need to be ignored.
M0090Actor reference must have an actor type .let a = actor { ... }; a.someMethod();The variable a must have an explicit actor type to call its methods.
M0096Expression can't produce expected type.func getValue() : Bool { return "true"; }String cannot be returned where a Bool is expected.
M0097Expected function type.let x = 5; x();Cannot call a non-function value.
M0098Cannot instantiate function type.type Func = (Nat) -> Nat; let f = Func();Function types cannot be instantiated directly.
M0112Tuple pattern cannot consume type.let (x, y) = { a = 1; b = 2 };Cannot destructure an object using tuple pattern.
M0116Variant pattern cannot consume type.switch (value) { case (#ok(x)) { ... } }; (where value is not a variant)Cannot match a non-variant value with variant patterns.
M0126Shared function cannot be private.actor { private shared func publicAPI() { ... } }Shared functions must be public in actors.
M0137A type or class was declared that explicitly or implicitly references an outer type parameter.class Container<T>() { class Inner() { var value : T = ... } }Inner classes cannot reference type parameters from outer classes.
M0139Inner actor classes are not supported.class Outer() { actor class Inner() { ... } }Actor classes cannot be nested inside other classes.
M0141Forbidden declaration in program.import Prim "mo:prim";Certain imports/declarations are not allowed in normal user code.
M0145Pattern does not cover value.switch (option) { case (null) { ... } }; (missing ?some case)Switch statement doesn't handle all possible values.
M0149An immutable record field (declared without var) was supplied where a mutable record field (specified with var), was expected.type Mutable = { var x : Nat }; func set(r : Mutable) { ... }; set({ x = 10 });Immutable record provided where mutable was expected.
M0150A mutable record field (declared with var) was supplied where an immutable record field (specified without var) was expected.type Immutable = { x : Nat }; let record : Immutable = { var x = 10 };Mutable field provided where immutable was expected.
M0151A object literal is missing some fields.type Person = { name : Text; age : Nat }; let p : Person = { name = "John" };The age field is missing from the object literal.
M0153An imported Candid file (.did) mentions types that cannot be represented in Motoko.import Types from "types.did"; (where types.did contains incompatible types)The imported Candid file contains types that don't map to Motoko types.
M0154Deprecation annotation.@deprecated("Use newFunction instead") func oldFunction() { ... }Using a deprecated function or feature that has been marked with @deprecated.
M0155Inferred type Nat for subtraction.let x = 5 - 10;Subtraction with natural numbers can underflow without type annotation.
M0156A parameterized type definition, or set of type definitions, is too complicated for Motoko to accept.type Complex<A,B,C,D,E,F> = ... (extremely nested recursive types)Type definition too complex for the compiler to process.
M0157A type definition, or set of type definitions, is ill-defined.type RecursiveType = RecursiveType;A type directly references itself without proper indirection.
M0158A public class was declared without providing it with a name.public class<T>() { ... }Public parameterized classes must be named explicitly.
M0194An identifier was defined without referencing it later.func process() { let unused = computeValue(); }The variable unused is defined but never used.
M0195A function that demands elevated (system) capabilities was called without manifestly passing the capability.func splitCycles() { let amount = ExperimentalCycles.balance() / 2; ExperimentalCycles.add(amount);}}System capability function called without proper capability passing.
M0197A function that requires (system) capabilities was called in a context that does not provide them.func invalidCycleAddition(): () { ExperimentalCycles.add(1000);}Calling a function requiring system capabilities from a context without them.
M0198A field identifier was specified in an object pattern without referencing this identifier later.func process(obj) { let { id } = obj; }The extracted id field is never used in the function body.
Logo