The official AMPscript documentation is not always right. Return types can be wrong, arguments described as optional turn out to be mandatory, and some functions behave differently than the reference page claims. This page collects those cases for Marketing Cloud Engagement — every entry here is proven by running the function on a live Engagement CloudPage, not inferred from reading the docs.

AMPscript only — SSJS quirks live elsewhere

This page tracks AMPscript discrepancies. Server-Side JavaScript engine and runtime quirks are documented separately at ssjs.guide/engine-limitations/differs-from-docs.

Looking for Marketing Cloud Next?

Function availability and behaviour diverge between the two platforms — Char and RegExMatch, for example, are Engagement-only. Next findings are tracked on AMPscript Differs from Official Docs (Next).

How to read an entry

Each finding is a card with a severity (high / medium / low), a discrepancy-type code, the affected function category, a description of what the docs claim versus what actually happens, and — where one exists — a copy-paste AMPscript snippet that reproduces it plus a link to the official page that is inaccurate.

Code Discrepancy type
A1 Wrong return type
A2 Null / empty result on absence
A3 Wrong argument count or optionality
A4 Wrong, renamed, or relocated members
A5 Undocumented but real behaviour
A6 Context or availability differs
A7 Encoding, format, or validation semantics

Findings

Discrepancy type

Random — decimal bounds are documented as allowed but abort the page

High A7 encoding/format/validation semantics Math

The official reference presents the two bounds as numbers that may carry a decimal part. At runtime only whole numbers are usable: every decimal bound aborted the CloudPage with HTTP 422, discarding all output rendered before it. Random(1.2, 1.8), Random(1, 2.5), Random(1.0, 3.0) and the quoted form Random("1.5", "3.5") all failed, while the equivalent whole-number calls and whole-number strings such as Random("1", "2") returned values normally.

The same decimal calls were redeployed to the parent business unit and aborted there too, so this is not a limitation of the child QA account. Treat the bounds as integers only — round or truncate before the call.

Show test script
%%[ VAR @b
  SET @b = RequestParameter("b")
  /* whole-number bounds: renders a value */
  IF @b == "ok" THEN
    OutputLine(Concat("Random(1,3)=[", Random(1,3), "]"))
  ENDIF
  /* decimal bounds: page aborts with HTTP 422, nothing renders */
  IF @b == "dec" THEN
    OutputLine(Concat("Random(1.2,1.8)=[", Random(1.2,1.8), "]"))
  ENDIF
]%%

Official documentation

Divide — a zero divisor silently renders an infinity symbol instead of failing

Medium A5 undocumented-but-real members Math

The official page says nothing at all about a divisor of zero — this is an undocumented behaviour, not a contradiction of the docs. What actually happens is that the call does not raise an error and does not abort the page: with a non-zero dividend it renders the infinity symbol (U+221E), and Divide(0, 0) renders NaN. A negative dividend renders -∞. A zero divisor passed as a numeric string behaves identically.

That matters because the value flows straight into the rendered message, so an unguarded division by zero ships to the recipient rather than failing loudly. Guard the divisor yourself before dividing.

Note the glyph really is U+221E — a console that is not reading the response as UTF-8 can display it as the digit 8. Verify the codepoint before concluding anything about the rendered literal.

Show test script
%%[ VAR @b
  SET @b = RequestParameter("b")
  IF @b == "d0" THEN
    OutputLine(Concat("Divide(100,0)=[", Divide(100,0), "]"))
    OutputLine(Concat("Divide(-100,0)=[", Divide(-100,0), "]"))
    OutputLine(Concat("Divide(0,0)=[", Divide(0,0), "]"))
    OutputLine(Concat("Divide(100,'0')=[", Divide(100,"0"), "]"))
  ENDIF
]%%

Official documentation

Mod — a zero divisor returns NaN — inconsistently with its sibling Divide

Medium A5 undocumented-but-real members Math

As with Divide, the official page is silent on a divisor of zero, so this is undocumented behaviour rather than a documented claim being wrong. The call does not abort the page: Mod(10, 0), Mod(-10, 0) and Mod(0, 0) all render the three ASCII characters NaN.

The interesting part is the inconsistency between the two sibling functions. Divide renders the infinity symbol for a non-zero dividend and only falls back to NaN for 0 / 0, whereas Mod renders NaN in every zero-divisor case including a non-zero dividend — see Divide. Code that tests for one sentinel will not catch the other, so guard the divisor instead of pattern-matching the result.

The responses were fetched as UTF-8 so a non-ASCII glyph could not be mistaken for NaN.

Show test script
%%[ VAR @b
  SET @b = RequestParameter("b")
  IF @b == "m0" THEN
    OutputLine(Concat("Mod(10,0)=[", Mod(10,0), "]"))
    OutputLine(Concat("Mod(-10,0)=[", Mod(-10,0), "]"))
    OutputLine(Concat("Mod(0,0)=[", Mod(0,0), "]"))
    /* contrast: Divide renders the infinity symbol here */
    OutputLine(Concat("Divide(10,0)=[", Divide(10,0), "]"))
  ENDIF
]%%

Official documentation

Length — counts UTF-16 code units, so an emoji measures 2

Medium A7 encoding/format/validation semantics String

The official page speaks of the number of characters without ever saying what a character is, so nothing it claims is contradicted — this is simply undocumented. What the runtime actually counts is UTF-16 code units. A single emoji from outside the Basic Multilingual Plane returns 2, because it is stored as a surrogate pair, while a precomposed accented letter and the German sharp s each return 1.

That was settled by dumping the codepoints of the echoed input next to the returned count, so a mis-decoded console could not have invented the result: the emoji echoed as the pair 55357/56832 and still measured 2, the accented letter echoed as the single codepoint 233 and measured 1.

The practical consequence is truncation and validation logic. A field limited by Length will accept a string that is one user-visible character shorter than expected as soon as an emoji is involved, and cutting a string at a code-unit position can split a surrogate pair in half.

Show test script
%%[ VAR @b
  SET @b = RequestParameter("b")
  IF @b == "nal" THEN
    /* plain ASCII: one unit per character */
    OutputLine(Concat("NAL_ASCII4=[", Length("abcd"), "]"))
    /* precomposed accented letter: still counts as one */
    OutputLine(Concat("NAL_CAFE=[", Length("café"), "]"))
    /* astral emoji: counts 2, one per UTF-16 code unit */
    OutputLine(Concat("NAL_EMO=[", Length("😀"), "]"))
  ENDIF
]%%

Official documentation

Concat — boolean values are swallowed silently — the whole String family does it

Medium A5 undocumented-but-real members String

None of the String reference pages say what happens when a boolean reaches them, so this is undocumented behaviour and not a contradiction of anything the docs state. The runtime neither converts the boolean nor rejects it: the page returns HTTP 200 and the value simply disappears. Concat(true, false) rendered an empty result, and so did Lowercase(true) and Uppercase(false). Length(true) is the only one that renders anything at all, and it renders 0.

This is worse than an abort would be, because booleans arrive in string contexts by accident all the time — the result of IsNull, Empty or an address check dropped straight into a Concat for a rendered message. The send succeeds, the recipient gets a sentence with a hole in it, and nothing in the response signals a problem.

Because nothing coherent about the boolean survives, this is treated as a rejection for typing purposes: the parameter types stay string, number and date. Convert booleans to text yourself — an IF that writes "yes" or "no" into a variable — before passing them to a string function.

Show test script
%%[ VAR @b
  SET @b = RequestParameter("b")
  /* all three return HTTP 200 with nothing between the brackets */
  IF @b == "cb" THEN
    OutputLine(Concat("CB=[", Concat(true, false), "]"))
  ENDIF
  IF @b == "lob" THEN
    OutputLine(Concat("LOB=[", Lowercase(true), "]"))
  ENDIF
  IF @b == "ub" THEN
    OutputLine(Concat("UB=[", Uppercase(false), "]"))
  ENDIF
  /* Length does render a number, but it is always 0 */
  IF @b == "lb" THEN
    OutputLine(Concat("LB=[", Length(true), "]"))
  ENDIF
]%%

Official documentation

Mod — the sign of the result follows the dividend, not the divisor

Low A5 undocumented-but-real members Math

The official page does not state which operand decides the sign of the remainder, and languages disagree on this. Proven at runtime: the sign always follows the dividend (the first argument). Mod(-10, 3) gives -1, Mod(10, -3) gives 1, and Mod(-10, -3) gives -1.

That matches C-style truncated remainder semantics and JavaScript’s %, but it is the opposite of the floored-modulo convention used by languages such as Python, where -10 % 3 is 2. Porting a modulo expression into AMPscript from a floored-modulo language changes the result for negative dividends — normalise explicitly if you need a non-negative remainder.

Show test script
%%[ VAR @b
  SET @b = RequestParameter("b")
  IF @b == "mods" THEN
    OutputLine(Concat("Mod(10,3)=[", Mod(10,3), "]"))
    OutputLine(Concat("Mod(-10,3)=[", Mod(-10,3), "]"))
    OutputLine(Concat("Mod(10,-3)=[", Mod(10,-3), "]"))
    OutputLine(Concat("Mod(-10,-3)=[", Mod(-10,-3), "]"))
  ENDIF
]%%

Official documentation

Uppercase — the German sharp s is left alone instead of expanding to SS

Low A7 encoding/format/validation semantics String

The official page makes no statement about the German sharp s, so this is undocumented behaviour rather than a documented claim being wrong. At runtime the sharp s is returned untouched: uppercasing a six-letter word containing it yielded the codepoints 83, 84, 82, 65, 223, 69 — the 223 sat unchanged between the surrounding capitals, so the result is still six characters rather than the seven a caller expecting an SS expansion would get.

Other accented letters are mapped: the lowercase accented vowels 224, 233, 238, 245 and 252 came back as 192, 201, 206, 213 and 220. So the sharp s is a specific gap, not a sign that non-ASCII input is ignored wholesale.

Casing is also culture-invariant. The dotless i (codepoint 305) uppercases to plain ASCII I (73), not to the dotted capital a Turkish locale would produce. Do not rely on uppercasing to normalise text for comparison across locales — compare on a value you normalised yourself.

Show test script
%%[ VAR @b
  SET @b = RequestParameter("b")
  IF @b == "nau" THEN
    /* the sharp s survives uppercasing verbatim */
    OutputLine(Concat("NAU_UP_SS=[", Uppercase("Straße"), "]"))
    /* accented lowercase letters do map to accented capitals */
    OutputLine(Concat("NAU_UP_CAFE=[", Uppercase("café"), "]"))
  ENDIF
  IF @b == "tr" THEN
    /* invariant, not Turkish: the dotless i becomes a plain ASCII I */
    OutputLine(Concat("TR_UP_DOTLESS=[", Uppercase("ı"), "]"))
  ENDIF
]%%

Official documentation

Concat — a single argument is accepted, although two are presented as the minimum

Low A3 wrong arity/optionality String

The official reference presents the function as joining two or more values, and our own catalog encoded that as a minimum of two. At runtime one argument is enough: Concat("only") returned HTTP 200 and echoed only back unchanged. Passing no argument at all is rejected — that call aborted the page with HTTP 422 before the surrounding marker line rendered — so the real minimum is one, not zero and not two.

Treat this as an accepted form the documentation never mentions rather than as the documentation being wrong: an argument-count disagreement on its own is recorded in the catalog, not flagged as a doc contradiction. It is listed here because callers can genuinely write the one-argument form and it will work, which is not something the reference lets you predict.

The catalog was corrected accordingly during this verification run: the minimum argument count moved from two to one and the second parameter is now marked optional, so editor diagnostics no longer flag a valid call.

Show test script
%%[ VAR @b
  SET @b = RequestParameter("b")
  /* one argument: HTTP 200, the value comes back unchanged */
  IF @b == "c1" THEN
    OutputLine(Concat("--- c1 start ---"))
    OutputLine(Concat("C1=[", Concat("only"), "]"))
    OutputLine(Concat("--- c1 done ---"))
  ENDIF
  /* zero arguments: page aborts with HTTP 422, no marker renders */
  IF @b == "c0" THEN
    OutputLine(Concat("--- c0 start ---"))
    OutputLine(Concat("C0=[", Concat(), "]"))
  ENDIF
]%%

Official documentation