Daniel Tull: Blog

Improving result builder failures using @available

Friday, 07 March 2025

Result builders in Swift are a great API for creating DSLs (Domain Specific Languages). In recent projects, we have used them to capture details about telemetry events and defining a set of feature flags that can be locally overridden by developers.

We have a Flag type that is generic over the value the feature flag will return. We also have OverridableFlag which defines a flag that can be overridden. It has the name and description from the flag, but also an array of options that can be used to override.

public struct OverridableFlag {
  public let name: Flag.Name
  public let description: Flag.Description
  public let options: [Option]
}

We build up a set of overridable flags in an OverridableFlags type which provides a nice named “home” for the overridable flags to live in.

extension OverridableFlags {

  @resultBuilder
  public enum Builder {

    public static func buildPartialBlock(first: OverridableFlag) -> OverridableFlags {
      OverridableFlags(values: [first])
    }

    public static func buildPartialBlock(accumulated: OverridableFlags, next: OverridableFlag) -> OverridableFlags {
      OverridableFlags(values: Array(accumulated) + [next])
    }
  }
}

However, astute readers may notice that there’s no public initialiser on the OverridableFlag type, so how do users add their flags into our system?

The answer lies within another part of the API for result builders, they allow the ability to convert an expression to the input type for a build block. We’ve decided that as users of this system only really see the Flag, keeping the OverridableFlag an implementation detail is nice.

Most flags in our system have boolean values, so the first buildExpression function is one that takes a Flag<Bool>. Internally this uses an overridable function on Flag which takes an option builder.

extension OverridableFlags.Builder {

  public static func buildExpression(_ flag: Flag<Bool>) -> OverridableFlag {
    flag.overridable {
      Option(name: "true", json: true)
      Option(name: "false", json: false)
    }
  }
}

This allows users to define a set of overridable flags using the boolean flags they already have defined elsewhere in the system.

OverridableFlags {
  Flag.shouldShowSomething
  Flag.shouldAllowAction
  ...
}

When a user attempts to use a flag which doesn’t have a boolean value, they will see the following compiler error. In the following case we’ve provided a String-based flag to the builder:

Cannot convert value of type ‘Flag' to expected argument type 'Flag'

This is great, the builder won’t accidentally take a flag it doesn’t support. However, it does nothing to guide the user to fixing their mistake.

Enter the @available attribute

The @available attribute in Swift is probably mostly used as a way to deprecate functions.

Another ability is using the unavailable argument, where instead of a deprecation warning, a compiler error is provided. Using this, we can add a buildExpression function that takes a flag of any value and mark it as unavailable.

Note: The boolean case above will still continue to work because Swift will always use the most specific function that it can see.

extension OverridableFlags.Builder {

  @available(*, unavailable, message: """
    Unsupported Flag type
    Use Flag.overridable with a set of options to create an overridable flag.
    """)
  public static func buildExpression<Value>(
    _ flag: Flag<Value>
  ) -> OverridableFlag {
    fatalError()
  }
}

Now when user’s of our library try to add a flag of an unsupported type they see a message that we can control to aid them to a solution for the problem:

‘buildExpression’ is unavailable: Unsupported Flag type Use Flag.overridable with a set of options to create an overridable flag.

This is really useful for result builder code where the default error message may make less sense to the caller.