The 4D language
Every entry on this page comes from a real incident. They share a shape: 4D does not warn you. It raises three levels away, or returns something plausible.
Null is not empty, and Null is not undefined
Num(Null) raises: "The arguments are not compatible." A freshly created ORDA entity has its numeric attributes at Null, not 0. A computed attribute doing Num(This.amount) makes the entity unreadable and undeletable — the history layer re-reads the previous version on .store() and hits the same error. Initialise every numeric before the first save.
String(Null) returns the text "null". So String(x) # "" is true for an empty field, and the guard you wrote does the opposite of what you meant. Test # Null first.
An absent property is undefined, not Null. A # Null guard protects nothing against a key that was never set. Test what you actually expect (Value type, or the key's presence).
A Null date compared to a literal raises. If (X.myDate # !00-00-00!) on a date attribute that was never filled gives "the arguments of this operation are not compatible". Go through a typed Date variable. Seen as a 500 on a call-action after the record had already been written — the data was saved and the redirect never happened.
A boolean field without never_null breaks If. Same family, same 500.
Operators
&& and || do not short-circuit, and precedence runs left to right. In A || B && C, 4D evaluates as it reads. Use parentheses, and never rely on the left side protecting the right: (X # Null) && (X.field = 1) still evaluates X.field.
A ternary passed as a call argument gives "a function is expected". Assign it to a variable first.
Collections
String() or Replace string() on a collection raises "the arguments are not compatible" — which surfaces as a 500, a dead template or a field that vanished. Test Value type = Is collection before converting. Note that el.value on a multiple select returns only the first option, which is how a collection ends up where a string was expected.
union() does not exist. Use concat() (new collection) or combine() (in place). Calling union() makes the calling block fail.
collection.query() only works on objects, not on scalars.
Typed variables and shared objects
A typed variable is checked before your type test. A loop variable or a declared parameter raises on assignment, before the Case of that was supposed to detect the type. Receive the value untyped and test it, or do not type it.
Fill a shared object outside the Use. Building the content inside raises a 500: build it, then use Use only to assign. OB Copy(…; ck shared) is the reliable form.
For each over a shared object is not allowed in the contexts where BWEB renders — build a plain copy first.
Files and folders
File(Application file) returns Null without fk platform path, and a Try swallows it: the file you expected simply never appears.
.parent raises above a 4D system folder. The function returns Null and the error surfaces three levels up, far from the cause. For git operations, do not walk up at all — git finds its own root.
File.name strips the extension. Use fullName when you need it.
Numbers and dates from the web
Num() breaks decimals coming from a form — use BSPK_STRING_TO_REAL.
A date stored as ISO text in APP_PARAMETERS comes back as a 4D Date from Storage. A For each typed as Text then raises error 54, and JSON Stringify lies about what is in there.
Never re-parse a date with a fixed-position Substring.
The one that hides all the others
A syntax error in one function makes the whole class unloadable. Every other function of that class disappears, bspk.log says nothing, and the symptom is a generic 500 — or a listbox that renders empty. Only the 4D compiler window names the error.
Reflex: when several unrelated things break at once, suspect one broken class before suspecting your change.
See also
- 4D in BWEB — traps specific to rendering and to component code
- Events and the POST — where several of these surface as "nothing happens"

