> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ubiquex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Conditional Expressions

> Use ?? , if/else, and nested conditions to write environment-aware infrastructure.

Infrastructure configuration is rarely uniform across environments. A production database needs more storage, higher availability, and longer backup retention than a dev database — but it's the same resource type, the same schema, the same stack. Duplicating entire stacks per environment is the wrong answer; parameterising every value as an input is tedious and leaks environment logic into the call site. Conditional expressions let you express environment-specific logic directly inside your `.iac` files, close to the resources they affect. ubx supports the null coalescing operator `??`, `if/else` expressions that return a value, `if` without an else (returning null when false), and nested `if/else` chains for multi-branch logic. All of these are `Resolved<T>` — they evaluate at compile time from inputs and locals, producing plain values that get inlined into the generated code.

## What you'll learn

* The `??` null coalescing operator — use an override if set, otherwise a default
* `if/else` expressions that return a value based on a condition
* `if` without an else — evaluates to null when the condition is false
* Nested `if/else` for multi-branch logic (ubx does not support `else if`)

***

## Why this matters

<Info>
  All conditional expressions in ubx are `Resolved<T>` — they evaluate at compile time from inputs and locals. This means the generated Pulumi code contains plain values, not runtime conditionals. The environment logic lives in your `.iac` source, not scattered across separate stack files or wrapper scripts.
</Info>

***

## The source code

<CodeGroup>
  ```hcl main.iac theme={"theme":"css-variables"}
  unit "aws_rds_instance" "db" {
    allocated_storage       = local.allocated_storage
    engine                  = "postgres"
    engine_version          = "15"
    instance_class          = local.instance_class
    username                = "admin"
    password                = "placeholder"
    skip_final_snapshot     = true
    backup_retention_period = local.backup_retention
    multi_az                = if input.env == "prod" { true } else { false }
  }
  ```

  ```hcl locals.iac theme={"theme":"css-variables"}
  # ?? — null coalescing: use the override if set, otherwise the default.
  local "instance_class" {
    value = input.override_class ?? "db.t3.micro"
  }

  # if/else — pick a value based on a condition.
  local "allocated_storage" {
    value = if input.env == "prod" { 100 } else { 20 }
  }

  # if (no else) — evaluates to null when condition is false.
  local "multi_az_note" {
    value = if input.env == "prod" { "multi-AZ enabled for prod" }
  }

  # Nested if/else — pick from more than two options.
  # ubx does not support else if — use a nested if inside the else branch.
  local "backup_retention" {
    value = if input.env == "prod" {
      30
    } else {
      if input.env == "staging" {
        7
      } else {
        1
      }
    }
  }
  ```

  ```hcl inputs.iac theme={"theme":"css-variables"}
  input "env" {
    type    = "string"
    default = "dev"
  }

  input "override_class" {
    type    = "string"
    default = null
  }
  ```

  ```hcl outputs.iac theme={"theme":"css-variables"}
  output "instance_class_used" {
    value = local.instance_class
  }
  ```

  ```hcl ubx.iac theme={"theme":"css-variables"}
  ubx {
    name    = "conditional-expressions"
    runtime = "typescript"
    stacks  = ["dev", "staging", "prod"]
  }

  backend "local" {
    path = ".ubx/state"
  }
  ```
</CodeGroup>

***

## How it works

<Steps>
  <Step title="Expressions are evaluated in the IR pass">
    During AST → IRProgram, all conditional expressions are evaluated against the resolved input values. The result is a plain `IRValue` — a string, number, or bool. No conditional logic appears in the generated code.
  </Step>

  <Step title="?? short-circuits on null">
    `input.override_class ?? "db.t3.micro"` evaluates the left side first. If the value is non-null, it's used. If it's null (the default), the right side is used. This is the cleanest pattern for optional overrides.
  </Step>

  <Step title="if/else returns a value">
    Unlike many languages where `if` is a statement, ubx's `if/else` is an expression — it always produces a value. Both branches must return the same type. The result is inlined directly into the attribute that uses it.
  </Step>

  <Step title="Nested if/else handles multiple branches">
    ubx does not have `else if` as a keyword. Multi-branch logic uses a nested `if` inside the `else` branch. The compiler flattens this into a simple ternary chain in the generated code.
  </Step>
</Steps>

***

## What ubx generates

<CodeGroup>
  ```typescript index.ts theme={"theme":"css-variables"}
  // Generated by ubx — do not edit
  import * as pulumi from "@pulumi/pulumi";
  import * as aws from "@pulumi/aws";

  const config = new pulumi.Config();
  const env = config.get("env") || "dev";
  const overrideClass = config.get("overrideClass") ?? null;

  const instanceClass = overrideClass ?? "db.t3.micro";
  const allocatedStorage = env === "prod" ? 100 : 20;
  const backupRetention = env === "prod" ? 30 : env === "staging" ? 7 : 1;

  const db = new aws.rds.Instance("db", {
      allocatedStorage: allocatedStorage,
      engine: "postgres",
      engineVersion: "15",
      instanceClass: instanceClass,
      username: "admin",
      password: "placeholder",
      skipFinalSnapshot: true,
      backupRetentionPeriod: backupRetention,
      multiAz: env === "prod",
  });

  export const instanceClassUsed = instanceClass;
  ```

  ```python __main__.py theme={"theme":"css-variables"}
  # Generated by ubx — do not edit
  import pulumi
  import pulumi_aws as aws

  config = pulumi.Config()
  env = config.get("env") or "dev"
  override_class = config.get("overrideClass")

  instance_class = override_class if override_class is not None else "db.t3.micro"
  allocated_storage = 100 if env == "prod" else 20
  backup_retention = 30 if env == "prod" else (7 if env == "staging" else 1)

  db = aws.rds.Instance("db",
      allocated_storage=allocated_storage,
      engine="postgres",
      engine_version="15",
      instance_class=instance_class,
      username="admin",
      password="placeholder",
      skip_final_snapshot=True,
      backup_retention_period=backup_retention,
      multi_az=env == "prod",
  )

  pulumi.export("instance_class_used", instance_class)
  ```

  ```go main.go theme={"theme":"css-variables"}
  // Generated by ubx — do not edit
  package main

  import (
      "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/rds"
      "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
      "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
  )

  func main() {
      pulumi.Run(func(ctx *pulumi.Context) error {
          cfg := config.New(ctx, "")
          env := cfg.Get("env")
          if env == "" {
              env = "dev"
          }
          overrideClass := cfg.Get("overrideClass")

          instanceClass := "db.t3.micro"
          if overrideClass != "" {
              instanceClass = overrideClass
          }
          allocatedStorage := 20
          if env == "prod" {
              allocatedStorage = 100
          }
          backupRetention := 1
          if env == "prod" {
              backupRetention = 30
          } else if env == "staging" {
              backupRetention = 7
          }

          _, err := rds.NewInstance(ctx, "db", &rds.InstanceArgs{
              AllocatedStorage:      pulumi.Int(allocatedStorage),
              Engine:                pulumi.String("postgres"),
              EngineVersion:         pulumi.String("15"),
              InstanceClass:         pulumi.String(instanceClass),
              Username:              pulumi.String("admin"),
              Password:              pulumi.String("placeholder"),
              SkipFinalSnapshot:     pulumi.Bool(true),
              BackupRetentionPeriod: pulumi.Int(backupRetention),
              MultiAz:               pulumi.Bool(env == "prod"),
          })
          if err != nil {
              return err
          }

          ctx.Export("instanceClassUsed", pulumi.String(instanceClass))
          return nil
      })
  }
  ```
</CodeGroup>

***

## Common mistakes

<Warning>
  ubx does not support `else if` as a keyword. Writing `if cond1 { } else if cond2 { }` is a parse error. Use a nested `if` inside the `else` branch instead: `if cond1 { } else { if cond2 { } else { } }`.
</Warning>

<Tip>
  `if` without an `else` evaluates to `null` when the condition is false. This is useful for optional attributes — set them only when a condition is met, and the attribute is omitted entirely when null. Make sure the resource attribute accepts null before using this pattern.
</Tip>

***

## Run it

```bash theme={"theme":"css-variables"}
ubx validate
ubx plan                           # dev defaults
ubx plan --var env=prod            # prod values
ubx plan --var override_class=db.r6g.large  # override instance class
```

***

## What you learned

<Check>`??` returns the left side if non-null, otherwise the right side — clean pattern for optional overrides</Check>
<Check>`if/else` is an expression in ubx — it returns a value and can be used anywhere a value is expected</Check>
<Check>`if` without `else` returns null when the condition is false</Check>
<Check>Multi-branch logic uses nested `if/else`, not `else if`</Check>

***

## Next steps

<CardGroup cols={2}>
  <Card title="for_each over a list" href="/v1/tutorials/06-for-each-list">
    Create multiple resources from a list
  </Card>

  <Card title="Language reference: conditionals" href="/v1/language/conditionals">
    Full conditional expression syntax
  </Card>
</CardGroup>

***

<Info>
  Full runnable example: [github.com/ubiquex/ubx-examples/05-conditional-expressions](https://github.com/ubiquex/ubx-examples/tree/main/05-conditional-expressions)
</Info>
