8.1 X++ Syntax, Control Flow & Global Functions

Key Takeaways

  • X++ supports nine primitive data types (boolean, int, int64, real, str, date, timeOfDay, utcdatetime, and guid), all of which initialize to deterministic zero/blank defaults without permitting null pointer references.
  • The container primitive is an immutable, 1-indexed value type storing heterogeneous primitives and nested containers, manipulated via conPeek, conPoke, conIns, conDel, conLen, and conNull.
  • Foundation collection classes (List, Map, Set, Array) are mutable reference objects instantiated with explicit Types:: element specifiers and traversed via dedicated Iterators (which permit in-flight deletions) or Enumerators (forward-only read traversal).
  • The X++ exception architecture intercepts runtime faults using try...catch blocks; catching Exception::Deadlock or Exception::UpdateConflict requires an aborted transaction state before invoking retry to re-execute the enclosing try scope.
  • The Global class provides ambient utility methods (info, warning, error, checkFailed, strFmt, systemDateGet, today, date2StrUsr) accessible throughout the codebase without namespace or class qualification.
Last updated: September 2026

8.1 X++ Syntax, Control Flow & Global Functions

Quick Answer: X++ is an enterprise object-oriented language compiled to Microsoft .NET Common Intermediate Language (CIL). It features nine primitive data types (boolean, int, int64, real, str, date, timeOfDay, utcdatetime, guid) that initialize automatically to default non-null values. The container type is an immutable, 1-indexed value type used for heterogeneous data packets, operated on via functions such as conPeek, conPoke, conIns, conDel, and conLen. For dynamic object storage, developers use collection classes (List, Map, Set, Array) traversed by Iterators (which allow deletion during iteration) or Enumerators (read-only forward cursors). Exception management uses try...catch blocks where retry re-executes the try block, provided the database transaction has rolled back outside the transaction boundary. Ambient helpers from the Global class (info, warning, error, checkFailed, strFmt, systemDateGet, today) are available everywhere without qualification.


1. X++ Primitive Data Types & Default Value Semantics

X++ enforces strong typing across all variables. Unlike C# or Java where object references can evaluate to null, X++ primitive data types never hold null values. When declared, every primitive automatically initializes to a deterministic default state.

Overview of Primitive Data Types

Data TypeUnderlying Storage / RangeDefault Initialized ValueEnterprise Usage in Dynamics 365 F&O
boolean1 byte (evaluated as true or false)falseStatus flags, validation results, toggle switches
int32-bit signed integer (-2,147,483,648 to 2,147,483,647)0Counters, line numbers, non-extensible enum values
int6464-bit signed integer (-2^63 to 2^63 - 1)0Surrogate primary keys (RecId), partition keys, ledger entry identifiers
realArbitrary-precision decimal floating point0.0Monetary amounts, quantities, tax rates, inventory unit measurements
strUnicode character array (dynamic or fixed length)"" (empty string)Account numbers, customer names, descriptions, document codes
dateCalendar day, month, year01\01\1900 (represented as dateNull())Accounting dates, delivery schedules, document invoice dates
timeOfDayInteger seconds since midnight (0 to 86,399)0 (midnight 00:00:00)Shift start times, batch job execution windows
utcdatetimeISO 8601 composite (yyyy-mm-ddThh:mm:ss)1900-01-01T00:00:00Database record timestamps (CreatedDateTime, ModifiedDateTime)
guid128-bit Globally Unique Identifier{00000000-0000-0000-0000-000000000000}Integration message correlation IDs, telemetry tracking, dual-write keys

Technical Deep Dive: Primitive Semantics & Arithmetic Precision

  1. Arbitrary Precision with real: The real data type in X++ is designed specifically for financial calculations. Unlike standard IEEE floating-point types (like double or float in C#) that suffer from binary rounding errors when storing base-10 fractions, X++ real numbers maintain exact precision up to 16 significant digits, avoiding cumulative rounding errors in general ledger ledgers.
  2. int64 for Record Identification: The foundational backbone of the relational model in Dynamics 365 F&O is the 64-bit integer surrogate key named RecId. Any variable storing or referencing table row IDs must be explicitly declared as int64 or an Extended Data Type (EDT) that extends RefRecId.
  3. date vs. utcdatetime: A date variable is timezone-agnostic and stores only the calendar date. In contrast, utcdatetime records the exact instant in coordinated universal time. When displayed on UI forms, the AOS framework automatically shifts utcdatetime values to match the current user's preferred timezone configured in their user options.

2. The Container Data Type & Built-In Functions

A container is a specialized, primitive value type unique to X++. It represents an ordered, immutable sequence of heterogeneous primitive data items (strings, integers, reals, dates, guids) and even nested containers.

Key Architectural Characteristics of Containers

  • Immutable Value Semantics: Containers are passed by value, not by reference. When you assign or modify a container, the runtime performs copy-on-write semantics. Functions that alter a container return a completely new container instance rather than mutating the original buffer.
  • 1-Based Indexing: Unlike C# arrays or collection classes in other languages, container functions in X++ are strictly 1-indexed. The first element is located at index 1, not index 0.
  • Heterogeneous Elements: A single container can store a combination of data types: for example, an integer at index 1, a string at index 2, and a date at index 3.
  • Cannot Store Object References: Containers cannot store class instances, form references, or live database cursor table buffers. Attempting to pack an object into a container requires serializing the object (e.g., using SysPackExtensions or the Packable interface).

Built-in Container Functions

FunctionSignature & ParametersDescription & System Behavior
conPeekconPeek(container c, int index)Extracts and returns the element at position index (1-based). If index is out of bounds, returns null primitive default.
conPokeconPoke(container c, int index, anytype value)Replaces the element at position index with value and returns a new container. Does not alter c in place.
conInsconIns(container c, int index, anytype value, ...)Inserts one or more elements starting at position index, shifting subsequent elements right, and returns a new container.
conDelconDel(container c, int start, int count)Deletes count elements starting at position start and returns a new container.
conLenconLen(container c)Returns the number of top-level elements present in the container.
conNullconNull()Returns an empty container constant ([]).
conFindconFind(container c, anytype target)Searches for target in container c and returns its 1-based index, or 0 if not found.
// Demonstrating Container Operations and 1-based indexing
container customerPacket;

// Bracket syntax initialization
customerPacket = ["US-001", 1500.50, today()];

// 1-based extraction via conPeek
str  custAccount = conPeek(customerPacket, 1); // Yields "US-001"
real creditLimit = conPeek(customerPacket, 2); // Yields 1500.50
date createdOn   = conPeek(customerPacket, 3); // Yields current system date

// Modifying an element requires assigning the return of conPoke
customerPacket = conPoke(customerPacket, 2, 2500.00); // Replaces index 2

// Inserting a status code at index 2
customerPacket = conIns(customerPacket, 2, "Active");
// customerPacket is now: ["US-001", "Active", 2500.00, createdOn]
int elementCount = conLen(customerPacket); // Returns 4

[!WARNING] Performance Trap with Containers in Loops Because containers are immutable value types, calling conIns or conPoke repeatedly inside a large loop (e.g., thousands of iterations) triggers intensive memory reallocation and copying. For high-volume data structures, always use List, Array, or a temporary table instead of repeatedly resizing a container.


3. Foundation Collection Classes: List, Map, Set, and Array

Dynamics 365 F&O provides object-oriented collection classes located within the base runtime library. Unlike containers, collections are reference types that dynamically expand in memory without requiring full array copies upon element insertion.

Overview of Collection Classes

  • List: An ordered sequence of elements of a declared type (Types::String, Types::Integer, Types::Record, etc.). Duplicates are permitted. Elements are appended using .addEnd() or prepended using .addStart().
  • Set: A collection of unique elements of a declared type. Calling .add() returns true if the item was added, or false if the item already exists in the set.
  • Map: A key-value dictionary where keys and values have defined types. For example, new Map(Types::String, Types::Real). Calling .insert(key, value) adds or updates a pair; .lookup(key) retrieves the value; .exists(key) checks for existence.
  • Array: A fixed or dynamically expanding collection of homogeneous elements indexed starting at 1. Access is achieved via array.value(index, [newValue]).

Iterators vs. Enumerators

When traversing collections in X++, developers must select between Iterators and Enumerators. Choosing the wrong cursor type is a common source of runtime exceptions.

Architectural AttributeCollection Iterator (ListIterator, SetIterator, MapIterator)Collection Enumerator (ListEnumerator, SetEnumerator, MapEnumerator)
Cursor DirectionBidirectional traversal supported (more(), next())Forward-only traversal supported (moveNext())
Structural Modification During TraversalAllowed. Iterators support .delete() to safely remove the current item from the underlying collection during loop iteration.Strictly Prohibited. Modifying the collection during enumeration invalidates the cursor and throws an invalid operation exception.
Syntax StyleExplicit while (iterator.more()) { ... iterator.next(); }Idiomatic while (enumerator.moveNext()) { val = enumerator.current(); }
Memory & Performance OverheadHigher memory footprint; maintains complex pointers to collection nodes.Lightweight, highly optimized forward cursor; preferred for read-only iteration.
Instantiating Methodnew ListIterator(myList)myList.getEnumerator()
// Pattern 1: Read-only traversal with ListEnumerator (High Performance)
List salesOrders = new List(Types::String);
salesOrders.addEnd("SO-1001");
salesOrders.addEnd("SO-1002");

ListEnumerator enumerator = salesOrders.getEnumerator();
while (enumerator.moveNext())
{
    str currentOrder = enumerator.current();
    info(strFmt("Processing order: %1", currentOrder));
}

// Pattern 2: Dynamic deletion with SetIterator
Set customerSet = new Set(Types::String);
customerSet.add("US-001");
customerSet.add("DE-001");

SetIterator setIt = new SetIterator(customerSet);
while (setIt.more())
{
    if (setIt.value() == "DE-001")
    {
        // Safely delete item without breaking the traversal
        setIt.delete();
    }
    else
    {
        setIt.next();
    }
}

4. Control Flow Structures

X++ control flow statements dictate branching, condition evaluation, and looping.

Branching: if...else and switch...case

  • if...else: Evaluates standard boolean expressions. Short-circuit logic applies (&& stops evaluating on the first false; || stops evaluating on the first true).
  • switch...case: Evaluates an expression against constant branches. X++ supports matching on integers, enums, strings, and containers.
    • Multiple case expressions can be stacked to execute the same logic block.
    • If break; is omitted at the end of a case block, execution falls through into subsequent cases until a break; or block exit is encountered.
switch (salesTable.SalesStatus)
{
    case SalesStatus::Backorder, SalesStatus::Delivered:
        info("Order is open or delivered; invoice processing pending.");
        break;
        
    case SalesStatus::Invoiced:
        info("Order is fully settled.");
        break;
        
    case SalesStatus::Canceled:
        warning("Order was canceled.");
        break;
        
    default:
        error("Unknown order state.");
        break;
}

Looping: while, for, and do...while

  • while (condition): Evaluates condition before executing the loop body.
  • for (init; condition; increment): Standard indexed loop counter.
  • do { ... } while (condition);: Executes the body at least once before evaluating the condition.
  • break and continue: break immediately terminates the innermost loop; continue skips the remainder of the current iteration and re-evaluates the loop condition.

5. Exception Handling Architecture & the retry Mechanism

Exception handling in X++ provides structured recovery from hardware failures, network timeouts, database concurrency collisions, and business validation errors. All exception processing relies on the try, catch, and retry statements.

The Exception Enumeration

Exception MemberRoot Cause & Runtime Behavior
Exception::DeadlockThrown by SQL Server when two concurrent transactions hold mutual locks on shared rows. The database engine kills one transaction as the deadlock victim.
Exception::UpdateConflictOccurs under Optimistic Concurrency Control (OCC) when a record's RecVersion changes between the read and update operations.
Exception::DuplicateKeyExceptionOccurs when an insert() violates a unique index constraint (AllowDuplicates = No).
Exception::ErrorStandard fatal business error thrown explicitly via throw Exception::Error or throw error("...").
Exception::Warning / InfoNon-fatal diagnostic messages added to the Infolog.
Exception::CLRErrorThrown when an unhandled .NET exception occurs during an external C# or CLR assembly call. Must inspect CLRInterop::getLastException().

The Mechanics of retry

The retry statement is a unique X++ construct that restarts code execution from the very first line of the immediately enclosing try block. It is primarily used to recover from transient database issues (Exception::Deadlock and Exception::UpdateConflict).

[!IMPORTANT] Critical Exam Rule: Transaction Boundaries and the retry Statement A retry command can only be called from within a catch block. Furthermore, retry cannot execute inside an active database transaction. If a deadlock or update conflict occurs inside a ttsbegin...ttscommit scope, the transaction is automatically placed in an aborted state (ttsabort). Therefore, the try block must wrap the ttsbegin statement outside the transaction. If retry is invoked while appl.ttsLevel() > 0, the runtime throws a fatal kernel error.

Resilient Concurrency Retry Pattern

public static void processOrderWithRetry(SalesId _salesId)
{
    #define.MaxRetries(3)
    int retryCount = 0;

    // The try statement MUST precede ttsbegin
    try
    {
        ttsbegin;

        SalesTable salesTable = SalesTable::find(_salesId, true); // Select forUpdate
        if (salesTable)
        {
            salesTable.CustAccount = "US-002";
            salesTable.update();
        }

        ttscommit;
    }
    catch (Exception::UpdateConflict)
    {
        if (retryCount < #MaxRetries)
        {
            retryCount++;
            // Relinquish thread CPU briefly to allow conflicting transaction to clear
            sleep(50);
            retry; // Jumps back to 'try' and restarts ttsbegin
        }
        else
        {
            throw error(strFmt("Failed to update order %1 after %2 conflict attempts.", _salesId, #MaxRetries));
        }
    }
    catch (Exception::Deadlock)
    {
        if (retryCount < #MaxRetries)
        {
            retryCount++;
            sleep(100);
            retry;
        }
        else
        {
            throw error("Transaction deadlocked permanently.");
        }
    }
    catch (Exception::Error)
    {
        // Non-transient business exception; do not retry
        error("Encountered non-recoverable business error during order processing.");
    }
}

6. Ambient Global Utility Methods & Infolog Management

The X++ runtime library features a static system class named Global. All methods defined on Global are automatically imported into the ambient execution scope of every X++ class, form, and table. Developers invoke these methods directly without writing Global::methodName().

Infolog Notification Methods

  1. info(str message): Posts an informational message (blue info glyph) to the user's Infolog drop-down.
  2. warning(str message): Posts a warning message (yellow triangle) indicating potential issues that did not abort the transaction.
  3. error(str message): Posts a business error (red cross) to the Infolog. Writing error("...") logs the message but does not immediately abort code execution unless paired with a throw statement (throw error("...")).
  4. checkFailed(str message): A composite helper that logs an error message via error(message) and immediately returns false. This method is the universal enterprise idiom inside table validation methods (validateField(), validateWrite(), validateDelete()).
// Textbook usage of checkFailed in validateWrite
public boolean validateWrite()
{
    boolean ret = super();

    if (this.CreditLimit < 0)
    {
        ret = checkFailed("@ABC:CreditLimitNegativeError");
    }

    return ret;
}

Ambient String and Date Helper Functions

  • strFmt(str formatString, anytype arg1, ... arg10): Merges arguments into positional tokens (%1, %2, up to %10). Replaces String.Format in .NET.
  • systemDateGet() vs. today():
    • systemDateGet(): Returns the current session business date configured in Dynamics 365. Users processing retroactive financial invoices can change their session date to a prior date. Transaction posting logic must always use systemDateGet().
    • today(): Returns the actual hardware clock date of the executing computer or server tier, ignoring session date overrides.
  • date2StrUsr(date d): Converts a date value to a string formatted according to the regional preferences and date mask configured for the current authenticated user.

7. Scenario Walk-Through: Resilient Batch Currency Rate Ingestion

Scenario Description

An automated integration job executes every hour to ingest foreign exchange (FX) rates from an external bank API. The inbound data arrives as an array of currency pairs and exchange rates. During heavy batch posting, concurrent ledger processes frequently lock ExchangeRate table records, resulting in intermittent Exception::UpdateConflict collisions. The integration must parse the rates into a typed Map, validate each record using checkFailed, and handle concurrency collisions with a maximum of 3 retries before escalating.

Step-by-Step Implementation Flow

  1. Declare Typed Collection: Create a Map of type [Types::String, Types::Real] to store currency-rate key-value pairs.
  2. Wrap in Resilient Retry Loop: Establish an outer try...catch (Exception::UpdateConflict) boundary with a retry counter.
  3. Validate Business Constraints: Call checkFailed if exchange rates are zero or negative.
  4. Execute Atomic Persistence: Perform updates inside ttsbegin...ttscommit.
public class CurrencyRateIngestionService
{
    public static void ingestRates(container _rawPayload)
    {
        #define.MaxRetries(3)
        int retryCount = 0;

        // Step 1: Deserialize payload into a typed Map
        Map rateMap = new Map(Types::String, Types::Real);
        int len = conLen(_rawPayload);
        
        for (int i = 1; i <= len; i++)
        {
            container item = conPeek(_rawPayload, i);
            str pair       = conPeek(item, 1);
            real rate      = conPeek(item, 2);
            rateMap.insert(pair, rate);
        }

        // Step 2: Outer try block guarding the transaction boundary
        try
        {
            ttsbegin;
            
            MapEnumerator me = rateMap.getEnumerator();
            while (me.moveNext())
            {
                str currencyPair = me.currentKey();
                real exchangeVal = me.currentValue();

                if (exchangeVal <= 0.0)
                {
                    checkFailed(strFmt("@ABC:InvalidExchangeRate", currencyPair));
                    throw Exception::Error;
                }

                // High concurrency table update with forUpdate
                ExchangeRateTable rateTable;
                select firstonly forUpdate rateTable
                    where rateTable.CurrencyPair == currencyPair;

                if (rateTable)
                {
                    rateTable.RateValue = exchangeVal;
                    rateTable.EffectiveDate = systemDateGet(); // Business session date
                    rateTable.update();
                }
            }
            ttscommit;
            info("@ABC:CurrencyIngestionSuccess");
        }
        catch (Exception::UpdateConflict)
        {
            if (retryCount < #MaxRetries)
            {
                retryCount++;
                sleep(100 * retryCount); // Exponential backoff
                retry; // Re-executes from 'try', opening fresh ttsbegin
            }
            else
            {
                error(strFmt("@ABC:IngestionFailedAfterRetries", #MaxRetries));
                throw Exception::UpdateConflict;
            }
        }
    }
}

8. Real-World Exam Traps: Syntax, Collections & Exceptions

[!WARNING] Exam Trap 1: Off-by-One Errors with Containers (1-Based vs. 0-Based) Exam questions regularly present code snippets calling conPeek(myContainer, 0). In X++, containers are 1-indexed. Attempting to peek at index 0 returns an empty or null-equivalent primitive value, leading to silent calculation bugs or unexpected defaults. Always verify that container indexing begins at 1.

[!WARNING] Exam Trap 2: Modifying Collections During Enumerator Loops An exam question might show a while (listEnumerator.moveNext()) loop where the developer calls myList.addEnd(...) or removes an item. This throws an InvalidOperationException at runtime because enumerators require an immutable collection snapshot. To delete items during traversal, the code must use ListIterator or SetIterator with its native .delete() method.

[!WARNING] Exam Trap 3: Calling retry While Inside an Active Transaction Scope Questions testing concurrency recovery often place the try { ... } block inside an existing ttsbegin ... ttscommit block, and then invoke retry upon catching Exception::Deadlock. This causes a runtime crash. A transaction that encounters a deadlock or update conflict is aborted by the kernel; invoking retry when appl.ttsLevel() > 0 is strictly illegal. The try statement must always sit outside ttsbegin.

[!WARNING] Exam Trap 4: Confusing today() with systemDateGet() Questions regarding financial postings and journal validation will offer both today() and systemDateGet() as potential answers. Using today() ignores the user's ERP session date and uses the server's clock date, violating accounting audit rules. The correct method for business transactions is always systemDateGet().

Loading diagram...
X++ Exception Handling & Resilient Retry Lifecycle
Test Your Knowledge

A developer writes an X++ routine to inspect elements within an incoming container variable named orderInfo. The container was initialized with three items: an account string, a line count integer, and a real total. What is the correct syntax to retrieve the account string and update the total in accordance with X++ container rules?

A
B
C
D
Test Your Knowledge

A batch processing job in Dynamics 365 Finance and Operations encounters frequent transient Exception::Deadlock errors when updating customer records. How should the developer structure the transaction and exception handling logic to safely retry the operation?

A
B
C
D
Test Your Knowledge

A developer needs to iterate over a collection of customer account numbers in a List class instance and remove accounts that have an expired status during the loop. Which cursor construct must the developer use to perform deletions during traversal?

A
B
C
D
Test Your Knowledge

An enterprise financial integration service generates general ledger entries based on transaction batches. An accountant has adjusted their user session date in Dynamics 365 F&O to backdate invoices to the previous fiscal month. Which ambient Global method must the X++ code invoke to assign the voucher accounting date to match the user's active session date?

A
B
C
D