UltipaDocs
Try Playground
  • Quick Start
  • Procedure Management
  • Parameters and Types
  • Control Flow
  • Data Operations
  • Iterators and Traversal
  • Parallel Execution
  • Built-in Functions
  • Expressions
  • Algorithm Examples
  1. Docs
  2. /
  3. Stored Procedures

Control Flow

This page covers all control flow statements available in stored procedure bodies.

IF / ELSE IF / ELSE

Conditional branching:

Procedure Body Language
IF age > 18 {
    PRINT 'Adult'
} ELSE IF age > 12 {
    PRINT 'Teenager'
} ELSE {
    PRINT 'Child'
}

Conditions can use any expression that evaluates to a boolean:

Procedure Body Language
IF node.active = true AND OUT_DEGREE(node) > 5 {
    PRINT 'Active high-degree node'
}

IF score IS NOT NULL {
    RETURN score
}

IF SIZE(friends) > 0 {
    -- process friends
}

FOR Loop

Over a List

Procedure Body Language
FOR item IN [1, 2, 3, 4, 5] {
    PRINT item
}

Over a RANGE

Procedure Body Language
FOR i IN RANGE(0, 10) {
    PRINT i  -- prints 0 to 9
}

FOR i IN RANGE(1, 100) {
    -- process items 1 to 99
}

Over Nodes (SCAN)

Procedure Body Language
FOR node IN SCAN(:Person) {
    PRINT node.name
}

Over Edges (EDGES)

Procedure Body Language
FOR edge IN EDGES(:KNOWS) {
    PRINT edge._from || ' -> ' || edge._to
}

Over Neighbors

Procedure Body Language
FOR neighbor IN NEIGHBORS(node, OUT, :KNOWS) {
    PRINT neighbor.name
}

See Iterators and Traversal for full details on iterator sources.

PARALLEL FOR

Executes loop iterations across multiple worker goroutines:

Procedure Body Language
-- Auto-detect worker count
PARALLEL FOR node IN SCAN(:Person) {
    node.processed = true
}

-- Explicit worker count
PARALLEL FOR node IN SCAN(:Person) WORKERS 8 {
    LET score = OUT_DEGREE(node) * 0.1
    node.score = score
}

See Parallel Execution for complete parallel execution guide.

WHILE Loop

Procedure Body Language
LET i = 0
WHILE i < 10 {
    PRINT i
    i = i + 1
}

Iterates until convergence:

Procedure Body Language
LET changed = 1
LET iteration = 0
WHILE changed > 0 {
    LET changed = 0

    PARALLEL FOR node IN SCAN() WORKERS 8 {
        LET current = GET_SLICE_PROP(node._internal_id, 'rank')
        LET new_val = compute_new_rank(node)
        IF new_val <> current {
            SET_SLICE_PROP(node._internal_id, 'rank', new_val)
            LET changed = changed + 1
        }
    }

    LET iteration = iteration + 1
    PRINT 'Iteration ' || TOSTRING(iteration) || ': ' || TOSTRING(changed) || ' changed'
}

BREAK

Exit the innermost loop immediately:

Procedure Body Language
FOR i IN RANGE(1, 1000) {
    IF i > 50 {
        BREAK
    }
    PRINT i
}

Works with FOR, PARALLEL FOR, WHILE, and FOR...IN MATCH:

Procedure Body Language
FOR (node, depth) IN MATCH BFS (start)-[:KNOWS]->{1,10}(node) {
    IF depth > 5 {
        BREAK  -- stop traversal early
    }
    RETURN node._id, depth
}

CONTINUE

Skip the rest of the current iteration and move to the next:

Procedure Body Language
FOR i IN RANGE(1, 100) {
    IF i % 2 = 0 {
        CONTINUE  -- skip even numbers
    }
    PRINT i  -- only prints odd numbers
}
Procedure Body Language
FOR node IN SCAN(:Person) {
    IF node.active = false {
        CONTINUE  -- skip inactive nodes
    }
    -- process only active nodes
    RETURN node._id AS person_id
}

TRY / CATCH

Catches runtime errors and prevents them from terminating the procedure. If any statement in the TRY block fails, execution jumps to the CATCH block. If no error occurs, the CATCH block is skipped.

Basic TRY/CATCH

Procedure Body Language
TRY {
    LET result = risky_operation()
    PRINT result
} CATCH {
    PRINT 'Operation failed'
}

With Error Variable

Use CATCH (e) to capture the error. The error variable has two properties:

PropertyDescription
e.messageError message string
e.codeError code (e.g., EXECUTION_ERROR, USER_ERROR)
Procedure Body Language
TRY {
    LET val = 1 / 0
} CATCH (e) {
    PRINT 'Error: ' || e.message
    PRINT 'Code: ' || e.code
}

Execution Flow

When an error occurs in the TRY block:

  1. Remaining TRY statements are skipped
  2. The error is captured in the error variable (if provided)
  3. The CATCH block executes
  4. Execution continues normally after the TRY/CATCH block
Procedure Body Language
TRY {
    PRINT 'Step 1'       -- runs
    LET x = bad_call()   -- error here
    PRINT 'Step 2'       -- skipped
} CATCH (e) {
    PRINT 'Caught: ' || e.message  -- runs
}
PRINT 'Continues'        -- runs

Re-throwing Errors

Use bare THROW inside a CATCH block to re-throw the caught error. This is useful for logging or cleanup before propagating the error:

Procedure Body Language
TRY {
    dangerous_action()
} CATCH (e) {
    PRINT 'Logging error: ' || e.message
    THROW  -- re-throw the original error
}

Nested TRY/CATCH

TRY/CATCH blocks can be nested. Each block handles its own errors independently:

Procedure Body Language
TRY {
    TRY {
        LET x = risky_step_1()
    } CATCH (e1) {
        PRINT 'Step 1 failed: ' || e1.message
    }
    -- continues even if step 1 failed
    LET y = risky_step_2()
} CATCH (e2) {
    PRINT 'Step 2 failed: ' || e2.message
}

THROW

Raise an error explicitly:

Procedure Body Language
IF $iterations < 1 {
    THROW 'Iterations must be at least 1'
}

IF node IS NULL {
    THROW 'Node not found: ' || $node_id
}

Re-throw inside a CATCH block (no argument):

Procedure Body Language
TRY {
    process()
} CATCH (e) {
    -- cleanup...
    THROW  -- re-throw original error
}

ATOMIC Block

All statements inside execute as a single transaction. If any statement fails, all changes are rolled back:

Procedure Body Language
ATOMIC {
    INSERT (:Account {_id: 'a1', balance: 100})
    INSERT (:Account {_id: 'a2', balance: 200})
    INSERT (a1)-[:TRANSFER {amount: 50}]->(a2)
}

Use cases:

  • Multi-statement data mutations that must succeed or fail together.
  • Ensuring consistency when creating related nodes and edges.
  • Bank transfers, inventory updates, and other transactional operations.
Procedure Body Language
ATOMIC {
    MATCH (from:Account {_id: $from_id})
    MATCH (to:Account {_id: $to_id})
    SET from.balance = from.balance - $amount
    SET to.balance = to.balance + $amount
    INSERT (from)-[:TRANSFER {amount: $amount, timestamp: TIMESTAMP_MS()}]->(to)
}

Nesting

Control flow statements can be nested arbitrarily:

Procedure Body Language
FOR node IN SCAN(:Person) {
    IF OUT_DEGREE(node) > 10 {
        FOR neighbor IN NEIGHBORS(node, OUT, :KNOWS) {
            IF neighbor.active = true {
                TRY {
                    LET score = JACCARD_SIMILARITY(node, neighbor)
                    IF score > 0.5 {
                        RETURN node._id AS source, neighbor._id AS target, score
                    }
                } CATCH {
                    CONTINUE
                }
            }
        }
    }
}