Skip to content

FLIP 355: Cadence Guard Statement#356

Open
turbolent wants to merge 2 commits intomainfrom
bastian/cadence-guard-statement
Open

FLIP 355: Cadence Guard Statement#356
turbolent wants to merge 2 commits intomainfrom
bastian/cadence-guard-statement

Conversation

@turbolent
Copy link
Member

Work towards #355

@bluesign
Copy link
Collaborator

bluesign commented Feb 6, 2026

// With guard: clean and safe
fun processUser(name: String?, age: Int?): User? {
    guard let n = name else { return nil }
    guard let a = age else { return nil }

    return User(name: n, age: a)
}

I like it in general but syntax feels strange to me. But probably it is just me

edit: also piqued my interest // must exit: return, panic, break, or continue why it is like this?

@janezpodhostnik
Copy link
Contributor

I like it, but the guard keyword sounds just a bit off...

How about syntax like this:

if not let a = input.a { return nil;}

// the reasoning is maybe we would want something like this:
if let b = input.b && let c = input.c { //... }

// and then this:
if not let d = input.d && let e = input.e { //... }

@turbolent
Copy link
Member Author

I don't think we should try to come up with new syntax just for the sake of it, especially if there is already an established one we can borrow.

The Swift evolution's commonly rejected proposals has a section regarding guard, and how it is not just a negated if/unless/if not: https://github.com/swiftlang/swift-evolution/blob/main/commonly_proposed.md#control-flow-closures-optional-binding-and-error-handling

@zhangchiqing
Copy link
Member

I see three equivalent styles here for handling optionals:

  1. if let chaining (original idiomatic style)
fun processUser(name: String?, age: Int?): User? {
    if let n = name {
        if let a = age {
            return User(name: n, age: a)
        }
    }
    return nil
}
  • Fewer concepts to learn
  • Some nesting / rightward drift as more optionals are added
  1. Explicit nil checks + forced unwrap
fun processUser(name: String?, age: Int?): User? {
    if name == nil || age == nil {
        return nil
    }

    return User(
        name: name!,
        age: age!
    )
}
  • More verbose
  • Very explicit control flow
  • Easy to reason about for readers unfamiliar with optional binding (Note, User(name: name, age: age) will remind them about the mistake)
  1. guard let (new syntax)
fun processUser(name: String?, age: Int?): User? {
    guard let n = name else { return nil }
    guard let a = age else { return nil }

    return User(name: n, age: a)
}
  • Flattens control flow
  • Reads linearly with explicit early-exit
  • Requires learning a new construct

From my understanding, all three forms are semantically equivalent and compile down to the same short-circuiting logic.

Question:
Does guard let provide any meaningful performance or compilation advantages over if let or explicit nil checks, or is its primary for readability?

@turbolent
Copy link
Member Author

@bluesign

also piqued my interest // must exit: return, panic, break, or continue why it is like this?

Because that's the very feature of it: The else branch is the unhappy case and diverges, and must exit, so that the main branch can always assume that the condition held.

@zhangchiqing

Does guard let provide any meaningful performance or compilation advantages over if let or explicit nil checks, or is its primary for readability?

No, it does not have any performance/compilation advantages, its primary benefit is readability and safety.

@turbolent
Copy link
Member Author

turbolent commented Feb 6, 2026

Some additional context: We already have a form of guard let that is commonly used and I did not mention in the proposal: nil coalescing, e.g. let value = opt ?? panic("..."). However, this only allows panics and not e.g. a return from the function, and does not allow additional code/statements, e.g. emitting an event.

Another related language is Kotlin, which has the "elvis" operator ?:, which is similar to ?? in Cadence, and allows control-flow on the right-hand side, including return, e.g. val value = opt ?: return.

Copy link
Member

@jordanschalm jordanschalm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for writing this up. I think this feature would be helpful for managing optionals in larger functions.

Comment on lines +131 to +136
guard amount > 0.0 else {
panic("Amount must be positive")
}
guard amount <= self.balance else {
panic("Insufficient balance")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should probably be preconditions even if guard were implemented, so maybe not the best example

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, given the panics in the else branches, these should be pre-conditions. It's hard to come up with an example that is simple so it brings the point across, yet is also a real-world example.

Would something like this using returns be better?

access(all) fun maybeWithdraw(amount: UFix64): Bool {
    guard amount > 0.0 else {
        log("failed to withdraw: non-positive amount")
        return false
    }
    guard amount <= self.balance else {
        log("failed to withdraw: insufficient funds")
        return false
    }
    self.balance = self.balance - amount
    return true
}

Maybe with a note that this is just for illustration purposes, and a "production" withdraw function should not be implemented with a boolean return value.

@bluesign
Copy link
Collaborator

bluesign commented Feb 6, 2026

Because that's the very feature of it: The else branch is the unhappy case and diverges, and must exit, so that the main branch can always assume that the condition held.

@turbolent so this will be some kind of pre-condition but also will not fail ?

better examples can be constructed with failable cast or optional returning functions I guess.

guard let vault <- r as? @{FungibleToken.Vault} else {
   return nil
}
vault.withdraw(....)
guard let flowVaultReference = acc.storage.borrow<&Flowtoken.Vault>(from:/storage/flowTokenVault>() else {
  return nil
}

I think this is a bit shift in how we write Cadence also. ( in my opinion little conflicting with pre-conditions ) Like functions should return optionals instead of failing.

Also there are cases like this:

	var listed = false
	if let marketV3CollectionRef = acct.capabilities.borrow<&TopShotMarketV3.SaleCollection>(/public/topshotSalev3Collection) {

		let salePrice = marketV3CollectionRef.getPrice(tokenID: UInt64(5188888))

		if salePrice != nil {
			listed = true
		}
	} else if let marketV1CollectionRef = acct.capabilities.borrow<&Market.SaleCollection>(/public/topshotSaleCollection) {

		let salePrice = marketV1CollectionRef.getPrice(tokenID: UInt64(5188888))

		if salePrice != nil {
			listed = true
		}
	} 
	if listed {
		panic("moment is already listed for sale")
	}

which I think guard will not solve.

@turbolent
Copy link
Member Author

turbolent commented Feb 6, 2026

@bluesign

so this will be some kind of pre-condition but also will not fail ?

Like a pre-condition the condition must succeed, but unlike the pre-condition execution will not necessarily abort execution: the else branch may panic, which would be equivalent, but it may also just return from the function.

guard let vault <- r as? @{FungibleToken.Vault} else {
   return nil
}
vault.withdraw(....)

That's a good example! 👍

I think this is a bit shift in how we write Cadence also. ( in my opinion little conflicting with pre-conditions ) Like functions should return optionals instead of failing.

This proposal and feature do not intend to move developers away from pre-conditions or if they should write code that gracefully returns instead of aborting execution. In many cases pre-conditions are a better choice over guards, like pointed out by Jordan above (#356 (comment)).

But in cases where a pre-condition won't be possible, a guard can help, e.g. for unwrapping optionals (and then panicing or returning from the function, to be decided by the developer).

The proposal does try to recommend the early return pattern, and proposes a feature that allows expressing it elegantly.

Also there are cases like this: [...] which I think guard will not solve.

Yeah, in this example a guard would not help / be applicable.

@bluesign
Copy link
Collaborator

bluesign commented Feb 7, 2026

btw to make it clear, I like this feature (As people know me know well, I am big hater off forced unwrap operator ); now while reading my comments, it seems a bit negative, it is my general personality, always trying to find an edge case.

Copy link
Member

@SupunS SupunS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent proposal! I like the idea of having a way to early exit for "undesired" inputs, and keep the main execution path "clean". 👍

Comment on lines +88 to +90
guard <boolean-expression> else {
// must exit: return, panic, break, or continue
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variant of the syntax seems a bit redundant with the if !<expression> { ... }. My suggestion would be to only keep the below variant (which is very useful!), and defer this variant, so that all developers would stick to the same syntax to achieve similar use-cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already kind of addressed above: #356 (comment).

One can think of guard as a slightly stricter "if-not", which performs the same check, but in addition guarantees that the inverse is no longer true, as the else block is required to exit. his is useful for e.g. validating parameters, which are constant. Then again we already have pre-conditions which would be more appropriate in that scenario.

I also find it communicates the "checks" part of the function a bit more, but that is not a strong argument for it.

I'm also happy to leave this out of the proposal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it also seems redundant and I'd be fine leaving it out. If you feel strongly about it though, i'm fine with it because it doesn't really hurt to include it

@turbolent turbolent changed the title Add FLIP for Cadence guard statement FLIP 355: Cadence Guard Statement Feb 9, 2026
Copy link
Member

@joshuahannan joshuahannan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great addition! I'll definitely want to use this in a lot of places

Comment on lines +88 to +90
guard <boolean-expression> else {
// must exit: return, panic, break, or continue
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it also seems redundant and I'd be fine leaving it out. If you feel strongly about it though, i'm fine with it because it doesn't really hurt to include it

### Key Design Decisions

**Mandatory else block exit**:
The type checker verifies the else block always exits via return, panic, break, or continue.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not totally sure how continue works, but am I still allowed to continue out of a regular guard statement that is just in a function and not in a loop or will that not pass type checking?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continue statements can only be used in loops, this proposal does not change anything about that.

The line includes continue because it satisfies the requirement for exiting from the "main execution path": Once the continue statement is reached, the next iteration starts, and the code in the current iteration is no longer executed.

The implementation PR has some tests, which has good examples for where/how guards can be used: https://github.com/onflow/cadence/pull/4432/changes#diff-a54704e673343eb0960e4f06d4ba5d5de47c7dcd7a53085f9d0a1799de741062

From one of the tests:

// Sum of even numbers from 1 to 10: 2 + 4 + 6 + 8 + 10 = 30
fun test(): Int {
	var result = 0
	var i = 0
	while i < 10 {
		i = i + 1
		guard i % 2 == 0 else {
			continue
		}
		result = result + i
	}
	return result
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

Comments