Post
Share your knowledge.
How Do I represent optional Values in Move Using Option <T>?
I'm trying to understand this aspect of the Sui Network because I'm either building, debugging, or deploying something that touches this area. I want a detailed explanation of how this mechanism or feature works, along with relevant CLI usage, Move code structure, or architectural concepts. My goal is to gain enough clarity to apply this knowledge in a real project—whether that's a custom smart contract, an NFT system, a wallet integration, or a DeFi tool. The Sui Network has unique features compared to EVM chains, so I'm particularly interested in what sets it apart and how that affects development best practices. It would help to have sample code, command line examples, or typical errors to watch for, especially when using the Sui CLI, SDK, or deploying on localnet/testnet. Ultimately, I want to avoid common mistakes, follow the best security principles, and ensure that the functionality I’m working on behaves as expected under realistic conditions.
- Sui
- SDKs and Developer Tools
- Move
Answers
14In Move, optional values are represented using Option<T>, which is an enum type that can either contain a value of type T or be empty (None).
Key Concepts:
-
Option<T>has two variants:Some(T)– Contains a value of typeT.None– Represents the absence of a value.
Example Code:
module MyModule {
// Define an optional integer
public fun get_optional_value(): Option<u64> {
// Return Some with a value
return Option::Some(42);
// Alternatively, return None
// return Option::None<u64>();
}
public fun use_optional_value(opt: Option<u64>) {
match opt {
Option::Some(value) => {
// Do something with the value
std::debug::print(&value);
},
Option::None => {
// Handle absence of value
std::debug::print(&"No value");
}
}
}
}
CLI Usage:
You can pass an Option<T> as an argument in function calls, depending on the contract.
sui client call --function get_optional_value --package <package-id> --module <module-name>
Best Practices:
- Use
Option<T>for values that might not be present (e.g., optional fields, default values). - Properly handle both
SomeandNonecases in your code to avoid runtime errors.
In Move, you can represent optional values using the Option<T> type, which allows a value to either be present (Some<T>) or absent (None). This is similar to how option types are used in languages like Rust or Swift.
Key Concepts:
-
Option<T>is an enum type in Move that can either be:Some(T): Contains a value of typeT.None: Represents the absence of a value.
This type is useful when you need to handle cases where a value might not always be available, such as optional fields in a smart contract, or when a function might not always return a result.
Move Representation:
Here’s how Option<T> is defined and used in Move:
module MyModule {
// Define an Option type for a coin (for example)
struct Option<T> has store {
value: T,
is_some: bool,
}
// Function to create a Some value
public fun some<T>(value: T): Option<T> {
Option { value, is_some: true }
}
// Function to create a None value
public fun none<T>(): Option<T> {
Option { value: T, is_some: false }
}
// Function to check if an option has a value
public fun is_some<T>(opt: &Option<T>): bool {
opt.is_some
}
// Function to unwrap the value of an Option if it is Some
public fun unwrap<T>(opt: &mut Option<T>): T {
if (opt.is_some) {
move(opt).value
} else {
abort 1; // Abort if the Option is None
}
}
}
Explanation of Functions:
some<T>(value: T): Wraps a value inSome(value).none<T>(): Creates aNonevalue, representing the absence of a value.is_some<T>(opt: Option<T>): Checks if theOptionisSomeorNone.unwrap<T>(opt: Option<T>): Returns the value if it’sSome, or aborts the transaction if it’sNone.
Example Usage:
Here’s an example of how to use Option<T> in a smart contract, such as a coin transfer contract that might have an optional fee:
module CoinTransfer {
use 0x1::Option;
struct Coin {
balance: u64,
}
// Transfer coins, using an optional fee
public fun transfer_with_fee(
sender: &mut Coin,
receiver: &mut Coin,
amount: u64,
fee: Option<u64>
) {
// Deduct fee if it's Some, otherwise do nothing
if (Option::is_some(&fee)) {
let fee_value = Option::unwrap(&mut fee);
sender.balance = sender.balance - fee_value;
}
// Transfer the amount
sender.balance = sender.balance - amount;
receiver.balance = receiver.balance + amount;
}
}
Explanation:
Option<u64>for fee: The fee is an optional value that may or may not be provided. If it’sSome(fee), the transaction deducts the fee from the sender’s balance.is_someandunwrap: We check if the fee is present, and if it is, we apply it to the sender's balance.
CLI Example:
You interact with the contract through the Sui CLI to deploy and test the Move code.
# Publish the module
sui client publish --gas-budget 10000 --module path_to_module
# Call a function with an option parameter
sui client call --gas-budget 10000 --function transfer_with_fee --args <sender-address> <receiver-address> <amount> <option-fee> --network testnet
Where <option-fee> would be either Some(fee) or None depending on whether you want to provide a fee.
Common Errors:
- Incorrect Unwrapping: Trying to
unwrapaNonevalue will cause a transaction to abort. Always check withis_somebefore unwrapping. - Mismatched Types: Ensure that the type of the value inside
Optionmatches what’s expected in the function (e.g.,Option<u64>for optional fee amounts). - State Management: If you don’t correctly manage the state of
Option<T>, it can lead to runtime errors when interacting with resources or performing operations on empty values.
Best Practices:
- Always Check
is_some: Before accessing the value inside anOption, always check withis_someto avoid runtime errors. - Handle
NoneGracefully: When possible, define alternative behavior when anOptionisNone(like using default values). - Test on Testnet: Always test your
Option<T>handling logic on the Testnet to ensure there are no unintended side effects.
Summary:
Option<T> in Move allows you to represent optional values, similar to other languages. It's a powerful tool for smart contracts where certain fields or results may not always be present, like optional fees or parameters. Be sure to handle None values properly to avoid errors during transaction execution.
In Move, optional values are handled using the Option<T> type from the standard library (0x1::option), which enforces explicit null-checking through its Some(<T>) and None variants. Unlike Solidity's implicit nulls, Move requires pattern matching via option::is_some() and option::extract() to safely access wrapped values, preventing null reference errors at compile time. For Sui objects, this is particularly useful when dealing with dynamic fields or optional resource ownership, as it maintains type safety across network interactions. Common pitfalls include forgetting to check is_some() before extraction or mishandling nested Option types—always use the option module's functions rather than manual unwrapping.
In Move, the Option
- What is Option
in Move?
Option
some: T – Represents the presence of a value.
none – Represents the absence of a value.
use sui::option::{Self, Option};
let value: Option
- Declaring Optional Fields in Structs
struct UserProfile has key {
id: u64,
nickname: Option
This allows the nickname to be either present (Some(String)) or not (None).
- Pattern Matching
To work with the value inside Option
if (option::is_some(&user.nickname)) { let name = option::borrow(&user.nickname); // Use the name }
Or to extract:
if (option::is_some(&user.nickname)) { let name = option::extract(&mut user.nickname); // Do something with name }
Note: Once extracted, the Option becomes None.
- Best Practices
Avoid panics: Don’t extract without checking with is_some, as it may abort.
Default values: You can use a pattern like returning default when the value is absent.
Mutability: Option
- CLI and Build Considerations
When testing this logic:
sui move build sui move test
Or simulate via CLI with:
sui client call --function my_function --args 0x1 --package PACKAGE_ID --module my_module
- Common Errors to Avoid
Forgetting to check is_some before borrow or extract will abort.
Not initializing Option
Misunderstanding move semantics: once extracted, the value is gone.
- Example Module Snippet
module example::optional_demo { use sui::option::{Self, Option};
public fun new_profile(id: u64): UserProfile {
UserProfile {
id,
nickname: Option::none(),
}
}
public fun set_nickname(profile: &mut UserProfile, name: String) {
profile.nickname = Option::some(name);
}
public fun clear_nickname(profile: &mut UserProfile) {
profile.nickname = Option::none();
}
}
- Key Differences from EVM
Unlike Solidity’s null or zero values, Option
Using Option
Use std::option in Move to handle optional values with Option<T>. Here's how:
1. Basic Usage
use std::option;
// Declare an optional field
struct MyStruct {
value: option::Option<u64>
}
// Initialize with Some(value) or None
let some_value = option::some(42);
let none_value = option::none();
2. Pattern Matching
public fun get_value(opt: &Option<u64>): u64 {
if (option::is_some(opt)) {
option::destroy_some(option::extract(opt)) // Extract and destroy
} else {
0 // Default value
}
}
3. CLI Example
sui move test # Tests should cover Option cases
Key Differences from EVM
- Explicit Handling: Move forces you to handle
Nonecases (no null pointers) - Resource Safety:
Option<T>works with resources (unlike EVM's nullable references)
Common Pitfalls
- Memory Leaks: Forgetting to
destroy_somewhen extracting - Missing Checks: Not verifying
is_somebefore extraction
Best Practices
- Always initialize options explicitly
- Use helper functions:
public fun unwrap_or<T: copy>(opt: &Option<T>, default: T): T {
if (option::is_some(opt)) {
*option::borrow(opt)
} else {
default
}
}
Use Option<T> from 0x1::option to represent optional values in Sui Move (e.g., Option<u64>, Option<Object>).
use std::option::Option;
// Initialize
let x: Option<u64> = option::some(100);
let y: Option<u64> = option::none();
// Check and extract
if (option::is_some(&x)) {
let val = option::extract(&mut x); // Consumes the option
assert!(val == 100);
}
Key rules:
- Always check
is_some()beforeextract()to avoid aborts. - Use
borrow()to read without consuming:option::borrow(&x) - Never use
extract()onnone()— it aborts with code0x1. - Common in structs for nullable fields:
struct User has key { id: UID, reward: Option<Coin>, }
Best practice: Prefer Option over sentinel values (e.g., 0 for missing) and always handle both some and none cases to prevent runtime errors.
Use Option<T> in Move to handle optional values:
- Import –
use std::option. - Create –
some(value)ornone(). - Unwrap –
option::extract(&mut opt)(panics ifnone).
Example:
let maybe_value: Option<u64> = some(42); // or none()
Key Notes:
✔ Safer than EVM’s null (explicit handling required).
✔ Use is_none()/is_some() for checks.
Watch For:
- Unwrapping
none()crashes (useoption::borrowfor safe access). - Storage costs for wrapped types.
(Move enforces explicit optionality—no undefined behavior.)
1. Core Option<T> Implementation
Move's standard library provides an option module for handling optional values:
Basic Usage
module my_module::optional_data {
use std::option;
use sui::object::{Self, UID};
struct Container has key, store {
id: UID,
data: option::Option<u64> // T must have 'drop'
}
// Initialize with None
public fun new(ctx: &mut TxContext): Container {
Container {
id: object::new(ctx),
data: option::none()
}
}
// Set value (Some)
public fun set_data(container: &mut Container, value: u64) {
option::fill(&mut container.data, value);
}
// Unwrap with default
public fun get_data(container: &Container): u64 {
option::extract_with_default(&container.data, 0)
}
}
2. Key Operations
| Operation | Move Code | Notes |
|---|---|---|
| Create None | option::none() | Requires T: drop |
| Create Some | option::some(value) | Value consumed |
| Check exists | option::is_some(&opt) | Pure check |
| Unwrap | option::extract(&mut opt) | Panics if None |
| Safe Unwrap | option::extract_with_default(&opt, default) | Fallback value |
| Destroy | option::destroy_none(opt) | Free memory |
3. CLI Testing
Create Object with Option
sui client call \
--package 0xYOUR_PACKAGE \
--module optional_data \
--function new \
--gas-budget 5000000
Interact with Option
# Set value
sui client call \
--function set_data \
--args 0xCONTAINER_ID 42 \
--gas-budget 5000000
# Get value
sui client call \
--function get_data \
--args 0xCONTAINER_ID \
--gas-budget 5000000
4. Safety Patterns
Null Object Pattern
struct NullableNFT has key, store {
id: UID,
nft: option::Option<NFT>
}
public fun safe_transfer(
nullable: &mut NullableNFT,
recipient: address
) {
if (option::is_some(&nullable.nft)) {
let nft = option::extract(&mut nullable.nft);
transfer::public_transfer(nft, recipient);
}
}
Memory Management
public fun destroy(container: Container) {
let Container { id, data } = container;
object::delete(id);
option::destroy_none(data); // Required if T doesn't have drop
}
5. Real-World Examples
DeFi (Optional Rewards)
module defi::staking {
struct RewardState has key {
id: UID,
pending: option::Option<Coin<SUI>> // Optional reward
}
public fun claim_rewards(
state: &mut RewardState,
ctx: &mut TxContext
) {
if (option::is_some(&state.pending)) {
let reward = option::extract(&mut state.pending);
transfer::transfer(reward, sender(ctx));
}
}
}
NFT (Lazy Minting)
module nft::lazy {
struct MintTicket has key {
id: UID,
metadata: option::Option<vector<u8>> // Set later
}
public fun finalize(
ticket: &mut MintTicket,
metadata: vector<u8>
) {
assert!(option::is_none(&ticket.metadata), EALREADY_SET);
option::fill(&mut ticket.metadata, metadata);
}
}
6. Error Handling
Common Mistakes
| Error | Solution |
|---|---|
EOptionAlreadySet | Check is_none before fill |
EOptionIsNone | Use extract_with_default |
ECannotDestroy | Ensure T: drop or call destroy_none |
Defensive Programming
public fun safe_unwrap<T: drop>(opt: option::Option<T>): (bool, T) {
if (option::is_some(&opt)) {
(true, option::extract(&mut opt))
} else {
(false, option::destroy_none(opt))
}
}
7. Move vs EVM Comparison
| Feature | Move Option<T> | EVM Approach |
|---|---|---|
| Memory Safety | Compile-time checks | Manual null checks |
| Gas Cost | Fixed overhead | Variable (storage ops) |
| Type Safety | Enforced by VM | Developer responsibility |
Best Practices
- Always initialize options explicitly (
none()orsome()) - Use
extract_with_defaultfor fallback values - Clean up with
destroy_nonewhen needed - Test edge cases:
#[test] fun test_none_handling() { let opt = option::none<u64>(); assert!(option::is_none(&opt), 0); }
For production systems:
- Prefer compile-time checks over runtime
Option - Use distinct types instead of
Optionwhen possible (e.g.,struct Active; struct Inactive) - Monitor gas usage of nested
Optiontypes
To represent optional values in Move on the Sui Network, you use the generic enum type Option<T>, which is built into the Move standard library. This is conceptually similar to Option in Rust or Maybe in functional languages. It allows you to store either Some(value) or None, making it ideal for scenarios where a value might be absent—like an unset field or a missing lookup result.
In Sui Move, you use the option module (0x1::option) to work with this pattern. The Option<T> type is defined as:
enum Option<T> has copy, drop, store {
Some(T),
None,
}
You use it in structs and functions when a field or return value might be missing. Here’s an example to help you understand how it works practically:
use sui::option::{self, Option};
struct UserProfile has key {
id: UID,
nickname: Option<String>,
}
public fun set_nickname(profile: &mut UserProfile, name: String) {
profile.nickname = option::some(name);
}
public fun remove_nickname(profile: &mut UserProfile) {
profile.nickname = option::none<String>();
}
public fun has_nickname(profile: &UserProfile): bool {
option::is_some(&profile.nickname)
}
public fun get_nickname(profile: &UserProfile): &String {
option::borrow(&profile.nickname)
}
In this example, a UserProfile may or may not have a nickname. You can use option::some(value) to assign a value, option::none<T>() to remove it, option::is_some() to check if it's set, and option::borrow() to access it.
When working with the Sui CLI or SDK, be cautious when reading Option<T> values because the deserialization must match exactly. If using TypeScript SDK or Rust, optional values often map to null or custom wrapper types.
Best Practices:
- Always match against both
Some(_)andNoneto avoid logic gaps. - Use
option::unwrap()only when you're certain a value exists, otherwise it will panic (abort). - For dynamic data like optional fields in dApps (user bio, metadata), use
Option<T>to keep your structs flexible and avoid versioning the whole contract.
This pattern helps reduce null-pointer-like errors and aligns well with secure development practices on Sui. Learn more in the Sui Move stdlib docs on Option.
In Move, Option
Do you know the answer?
Please log in and share it.
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
- How to Maximize Profit Holding SUI: Sui Staking vs Liquid Staking616
- Why does BCS require exact field order for deserialization when Move structs have named fields?65
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution55
- Sui Move Error - Unable to process transaction No valid gas coins found for the transaction419
- Sui Transaction Failing: Objects Reserved for Another Transaction410