values that shape-shift under you

Type Errors in Dynamic Languages

Dynamic languages let any value flow anywhere; coercion and overloading turn + into concatenation and comparison into surprise. The mismatch hides until a specific input reaches a specific line.

A value's type is assumed, never checked, until an operator means something you didn't write.

01in the wild

In the wild

Coercion Makes + Ambiguous

Values shape-shift through coercion, so a + b means something different than you wrote.

example.js
// Coercion makes + ambiguous
1 + "1";        // "11"  (string concat)
"1" - 1;        // 0     (numeric subtraction)
[] + {};        // "[object Object]"
[] == ![];      // true  (the famous one)

// RIGHT: parse at the edge, then trust your types
const n = Number(input);
if (Number.isNaN(n)) throw new Error("not a number");
+ is overloaded for strings and numbers; the engine guesses. Parse input into a known type once, at the boundary.
// observed
1 + '1' = '11'
'1' - 1 = 0
parsed:  Number('1') = 1 (validated)
example.py
# Mixed types compare and concatenate unpredictably
"3" * 3          # '333', not 9
sorted([1, "2", 3])   # TypeError at runtime, deep in a loop

# RIGHT: type hints + a runtime guard at the edge
def area(w: float, h: float) -> float:
    w, h = float(w), float(h)     # coerce once, deliberately
    return w * h
Duck typing hides the mismatch until a specific input reaches a specific line. Coerce explicitly at the entry point.
// observed
'3' * 3 = '333'
sorted([1,'2',3]) -> TypeError
area('2','3') = 6.0 (coerced)
example.rb
# nil quietly threads through everything
user = {name: "Pam"}
user[:age] + 1        # NoMethodError: undefined `+' for nil

# RIGHT: fetch with a default, or fail loudly on purpose
age = user.fetch(:age, 0)
age = user.fetch(:age) { raise "age is required" }
[] returns nil for a missing key; the error surfaces far from the cause. fetch forces a decision about absence.
// observed
user[:age] + 1 -> NoMethodError on nil
user.fetch(:age, 0) + 1 -> 1
example.sh
# Unquoted, unset variables are the wild west
rm -rf $DIR/         # if DIR is empty: rm -rf /

# RIGHT: fail on unset vars, quote everything
set -euo nounset
: "${DIR:?DIR must be set}"
rm -rf "${DIR:?}/"   
An empty variable expands to nothing, turning a cleanup into a catastrophe. nounset and quoting are non-negotiable.
// observed
unset DIR: rm -rf / (disaster)
nounset:   'DIR must be set' -> script aborts

Truthiness Is Not Absence

Falsy values are not the same as missing values, but if (x) treats them identically.

example.js
// 0, "", NaN, null, undefined are ALL falsy
function setQuantity(q) {
  if (!q) q = 1;       // BUG: a real order of 0 becomes 1
  return q;
}

// RIGHT: test for absence, not falsiness
function setQuantity(q) {
  return q ?? 1;       // only null / undefined fall through
}
!q lumps a legitimate 0 in with null. The nullish coalescing operator distinguishes 'absent' from 'zero'.
// observed
setQuantity(0)  -> 1   (wrong)
q ?? 1 for 0    -> 0   (right)
example.py
def page_size(n):
    return n or 50        # BUG: page_size(0) -> 50

# RIGHT: be explicit about None vs a falsy-but-valid value
def page_size(n):
    return 50 if n is None else n
n or 50 fires for 0, [], and '' too. 'is None' asks the question you actually meant.
// observed
page_size(0) -> 50  (wrong)
is None form -> 0  (right)
02cross-pollination

Where this compounds

03weakness catalog

Mapped weaknesses (CWE)

On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.