Optional values and error handling

Safely handling optional values and error cases in Clarity smart contracts.

Clarity provides robust mechanisms for dealing with optional values and error cases. These features are crucial for writing secure and predictable smart contracts that can gracefully handle unexpected situations.

Why these functions matter

Clarity's optional value and error handling functions are designed with several important considerations in mind:

  1. Safety: They prevent unexpected null or undefined errors that could crash a contract.
  2. Explicitness: They force developers to consider and handle all possible outcomes.
  3. Readability: They make the code's intent clear, improving maintainability.
  4. Composability: They allow for clean function composition and chaining of operations.

Core optional and error handling functions

1. is-some and is-none

What: Check if an optional value contains a value (some) or is empty (none). Why: Essential for safely working with optional values before attempting to use them. When: Use when you need to check if an optional value is present before proceeding. How:

(is-some value)
(is-none value)

Best practices:

  • Always check optional values before unwrapping them.
  • Use in combination with unwrap functions for safe value extraction.

Example use case: Checking if a user exists in a map before performing an operation.

(define-map Users principal { balance: uint })

(define-public (check-balance (user principal))
  (if (is-some (map-get? Users user))
      (ok "User exists")
      (err "User not found")
  )
)

2. unwrap! and unwrap-panic

What: Extract the value from an optional or response type. Why: Allows safe extraction of values, with controlled behavior on failure. When: Use when you're certain a value exists or when you want to halt execution if it doesn't. How:

(unwrap! value error-expr)
(unwrap-panic value)

Best practices:

  • Use unwrap! when you want to provide a custom error message or value.
  • Use unwrap-panic sparingly, typically only in situations where failure is truly unexpected.

Example use case: Retrieving a user's balance, with a custom error if the user doesn't exist.

(define-map Users principal { balance: uint })

(define-public (get-balance (user principal))
  (ok (unwrap! (get balance (map-get? Users user)) (err "User not found")))
)

3. try!

What: Attempts to unwrap a response, returning early with an error if it fails. Why: Simplifies error handling in functions that return responses. When: Use when you want to propagate errors up the call stack. How:

(try! expression)

Best practices:

  • Use to chain multiple operations that might fail.
  • Helps keep code clean by avoiding nested if-else statements.

Example use case: Transferring tokens between users, with multiple checks.

(define-public (transfer (from principal) (to principal) (amount uint))
  (let
    (
      (senderBalance (try! (get-balance from)))
      (recipientBalance (try! (get-balance to)))
    )
    (try! (check-sufficient-balance senderBalance amount))
    (try! (update-balance from (- senderBalance amount)))
    (try! (update-balance to (+ recipientBalance amount)))
    (ok true)
  )
)

Practical example: safe token transfer system

Let's implement a simple token system that demonstrates the use of optional values and error handling:

(define-map Balances principal uint)

(define-read-only (get-balance (user principal))
  (default-to u0 (map-get? Balances user))
)

(define-public (transfer (to principal) (amount uint))
  (let
    (
      (senderBalance (get-balance tx-sender))
      (recipientBalance (get-balance to))
    )
    (if (>= senderBalance amount)
        (begin
          (try! (as-contract (stx-transfer? amount tx-sender to)))
          (map-set Balances tx-sender (- senderBalance amount))
          (map-set Balances to (+ recipientBalance amount))
          (ok true)
        )
        (err u1)
    )
  )
)

(define-public (deposit (amount uint))
  (let
    (
      (currentBalance (get-balance tx-sender))
    )
    (try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
    (ok (map-set Balances tx-sender (+ currentBalance amount)))
  )
)

This example demonstrates:

  1. Using try! to handle potential errors in the STX transfer operations.
  2. Proper error handling and propagation throughout the contract functions.

Conclusion

Proper handling of optional values and error cases is crucial for writing secure and reliable Clarity smart contracts. By leveraging Clarity's built-in functions like is-some, unwrap!, and try!, developers can create robust contracts that gracefully handle unexpected situations and provide clear feedback when errors occur. Always consider all possible outcomes and use these tools to make your contracts more predictable and maintainable.