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.
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. Thecontainertype is an immutable, 1-indexed value type used for heterogeneous data packets, operated on via functions such asconPeek,conPoke,conIns,conDel, andconLen. 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 usestry...catchblocks whereretryre-executes thetryblock, provided the database transaction has rolled back outside the transaction boundary. Ambient helpers from theGlobalclass (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 Type | Underlying Storage / Range | Default Initialized Value | Enterprise Usage in Dynamics 365 F&O |
|---|---|---|---|
boolean | 1 byte (evaluated as true or false) | false | Status flags, validation results, toggle switches |
int | 32-bit signed integer (-2,147,483,648 to 2,147,483,647) | 0 | Counters, line numbers, non-extensible enum values |
int64 | 64-bit signed integer (-2^63 to 2^63 - 1) | 0 | Surrogate primary keys (RecId), partition keys, ledger entry identifiers |
real | Arbitrary-precision decimal floating point | 0.0 | Monetary amounts, quantities, tax rates, inventory unit measurements |
str | Unicode character array (dynamic or fixed length) | "" (empty string) | Account numbers, customer names, descriptions, document codes |
date | Calendar day, month, year | 01\01\1900 (represented as dateNull()) | Accounting dates, delivery schedules, document invoice dates |
timeOfDay | Integer seconds since midnight (0 to 86,399) | 0 (midnight 00:00:00) | Shift start times, batch job execution windows |
utcdatetime | ISO 8601 composite (yyyy-mm-ddThh:mm:ss) | 1900-01-01T00:00:00 | Database record timestamps (CreatedDateTime, ModifiedDateTime) |
guid | 128-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
- Arbitrary Precision with
real: Therealdata type in X++ is designed specifically for financial calculations. Unlike standard IEEE floating-point types (likedoubleorfloatin C#) that suffer from binary rounding errors when storing base-10 fractions, X++realnumbers maintain exact precision up to 16 significant digits, avoiding cumulative rounding errors in general ledger ledgers. int64for Record Identification: The foundational backbone of the relational model in Dynamics 365 F&O is the 64-bit integer surrogate key namedRecId. Any variable storing or referencing table row IDs must be explicitly declared asint64or an Extended Data Type (EDT) that extendsRefRecId.datevs.utcdatetime: Adatevariable is timezone-agnostic and stores only the calendar date. In contrast,utcdatetimerecords the exact instant in coordinated universal time. When displayed on UI forms, the AOS framework automatically shiftsutcdatetimevalues 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 index0. - 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
SysPackExtensionsor thePackableinterface).
Built-in Container Functions
| Function | Signature & Parameters | Description & System Behavior |
|---|---|---|
conPeek | conPeek(container c, int index) | Extracts and returns the element at position index (1-based). If index is out of bounds, returns null primitive default. |
conPoke | conPoke(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. |
conIns | conIns(container c, int index, anytype value, ...) | Inserts one or more elements starting at position index, shifting subsequent elements right, and returns a new container. |
conDel | conDel(container c, int start, int count) | Deletes count elements starting at position start and returns a new container. |
conLen | conLen(container c) | Returns the number of top-level elements present in the container. |
conNull | conNull() | Returns an empty container constant ([]). |
conFind | conFind(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
conInsorconPokerepeatedly inside a large loop (e.g., thousands of iterations) triggers intensive memory reallocation and copying. For high-volume data structures, always useList,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()returnstrueif the item was added, orfalseif 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 viaarray.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 Attribute | Collection Iterator (ListIterator, SetIterator, MapIterator) | Collection Enumerator (ListEnumerator, SetEnumerator, MapEnumerator) |
|---|---|---|
| Cursor Direction | Bidirectional traversal supported (more(), next()) | Forward-only traversal supported (moveNext()) |
| Structural Modification During Traversal | Allowed. 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 Style | Explicit while (iterator.more()) { ... iterator.next(); } | Idiomatic while (enumerator.moveNext()) { val = enumerator.current(); } |
| Memory & Performance Overhead | Higher memory footprint; maintains complex pointers to collection nodes. | Lightweight, highly optimized forward cursor; preferred for read-only iteration. |
| Instantiating Method | new 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 firstfalse;||stops evaluating on the firsttrue).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 acaseblock, execution falls through into subsequent cases until abreak;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.breakandcontinue:breakimmediately terminates the innermost loop;continueskips 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 Member | Root Cause & Runtime Behavior |
|---|---|
Exception::Deadlock | Thrown by SQL Server when two concurrent transactions hold mutual locks on shared rows. The database engine kills one transaction as the deadlock victim. |
Exception::UpdateConflict | Occurs under Optimistic Concurrency Control (OCC) when a record's RecVersion changes between the read and update operations. |
Exception::DuplicateKeyException | Occurs when an insert() violates a unique index constraint (AllowDuplicates = No). |
Exception::Error | Standard fatal business error thrown explicitly via throw Exception::Error or throw error("..."). |
Exception::Warning / Info | Non-fatal diagnostic messages added to the Infolog. |
Exception::CLRError | Thrown 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
retryStatement Aretrycommand can only be called from within acatchblock. Furthermore,retrycannot execute inside an active database transaction. If a deadlock or update conflict occurs inside attsbegin...ttscommitscope, the transaction is automatically placed in an aborted state (ttsabort). Therefore, thetryblock must wrap thettsbeginstatement outside the transaction. Ifretryis invoked whileappl.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
info(str message): Posts an informational message (blue info glyph) to the user's Infolog drop-down.warning(str message): Posts a warning message (yellow triangle) indicating potential issues that did not abort the transaction.error(str message): Posts a business error (red cross) to the Infolog. Writingerror("...")logs the message but does not immediately abort code execution unless paired with athrowstatement (throw error("...")).checkFailed(str message): A composite helper that logs an error message viaerror(message)and immediately returnsfalse. 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). ReplacesString.Formatin .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 usesystemDateGet().today(): Returns the actual hardware clock date of the executing computer or server tier, ignoring session date overrides.
date2StrUsr(date d): Converts adatevalue 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
- Declare Typed Collection: Create a
Mapof type[Types::String, Types::Real]to store currency-rate key-value pairs. - Wrap in Resilient Retry Loop: Establish an outer
try...catch (Exception::UpdateConflict)boundary with a retry counter. - Validate Business Constraints: Call
checkFailedif exchange rates are zero or negative. - 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 callsmyList.addEnd(...)or removes an item. This throws anInvalidOperationExceptionat runtime because enumerators require an immutable collection snapshot. To delete items during traversal, the code must useListIteratororSetIteratorwith its native.delete()method.
[!WARNING] Exam Trap 3: Calling
retryWhile Inside an Active Transaction Scope Questions testing concurrency recovery often place thetry { ... }block inside an existingttsbegin ... ttscommitblock, and then invokeretryupon catchingException::Deadlock. This causes a runtime crash. A transaction that encounters a deadlock or update conflict is aborted by the kernel; invokingretrywhenappl.ttsLevel() > 0is strictly illegal. Thetrystatement must always sit outsidettsbegin.
[!WARNING] Exam Trap 4: Confusing
today()withsystemDateGet()Questions regarding financial postings and journal validation will offer bothtoday()andsystemDateGet()as potential answers. Usingtoday()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 alwayssystemDateGet().
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 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 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?
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?