Why Does My YAML Break? The Loud Errors and the Silent Ones
Two tickets land the same morning, both say 'my YAML broke.' One is a red parse error at line 12 — fixed in a minute. The other is a service that booted fine and behaved wrong, because an unquoted value quietly became something else. They're opposite problems, and the dangerous one is the quiet one.
Two tickets land the same morning, and both say the same thing: my YAML broke.
The first is a red parse error — the CI log points at line 12, “tabs are not allowed as indentation,” and it’s fixed in under a minute. The second is stranger: a service booted cleanly, no error anywhere, but it’s serving the wrong region, and after an hour of staring you find it — someone wrote region: NO for Norway, and the config loaded it as the boolean false. Same complaint, two completely different problems. And the second kind is the one that reaches production, precisely because nothing broke loudly enough to stop it.
That’s the thing to understand about YAML: “it broke” is two opposite failures wearing one sentence. One kind is loud — the parser refuses the file and tells you exactly where. The other is silent — the file parses fine and hands your program a value you never wrote. The loud kind wastes ten minutes; the silent kind ships. Once you can tell which one you’re looking at, YAML stops feeling haunted, because the two need opposite debugging moves. This is the sibling to the config-format pillar, which argued that YAML’s habit of guessing is the price of its convenience — here we cash out exactly how that guessing bites, and how to tell it apart from ordinary syntax errors.
The loud failures: YAML won’t parse (and that’s the good news)
When YAML refuses a file, count yourself lucky. The parser has caught the problem for you and handed you a line and column. These are annoying, not dangerous — you follow the pointer and fix the syntax. Here are the ones you’ll actually hit.
Tabs used for indentation. The single most common one. YAML forbids tab characters for indentation — structure is spaces only.
server:
host: localhost
That tab in front of host produces, verbatim:
YAMLParseError: Tabs are not allowed as indentation at line 2, column 1
The fix is exactly what the message says: replace the tab with spaces. This one bites hardest when an editor inserts tabs invisibly, or a copy-paste drags them in — the file looks indented correctly and only the parser can see the tab.
Inconsistent indentation. YAML infers your structure from how far each line is indented, so an extra or missing space changes what nests under what — and often becomes an error the parser can’t resolve.
database:
host: localhost
port: 5432
port is indented one space deeper than host, and the parser rejects the impossible nesting. The fix is to make siblings line up to the same column, exactly.
An unclosed quote or bracket. Flow collections ([...], {...}) and quoted strings must close.
ports: [80, 443
gives you Flow sequence … must be sufficiently indented (or a missing-bracket error, depending on what follows) — and name: "prod gives Missing closing "quote. Both name the line; both are fixed by closing the thing you opened.
Duplicate keys. Two keys with the same name in one mapping is ambiguous, and a strict parser rejects it outright:
region: us-east-1
region: eu-west-1
→ Map keys must be unique at line 2, column 1. (Not every parser is this strict — some silently keep the last value, which quietly slides this into the other category. When your parser does reject it, it’s doing you a favor.)
For every failure in this section the move is identical: read the error, go to the line and column it names, fix the syntax there. The parser is on your side. Now for the half where it isn’t.
The silent failures: YAML parses fine and hands you the wrong value
Here there is no red error, no line number, nothing to follow. The file parsed “successfully.” The problem is that successfully meant something other than what you wrote — a string turned into a boolean, an empty key turned into null, a version turned into a number. Your program starts with bad data and fails somewhere far away from the actual cause. This is the expensive kind, and it comes in a few flavors.
The missing space after a colon. A key needs a colon followed by a space (or a newline). Miss the space and you don’t get an error — you get a plain string:
timeout:30
parses to the string "timeout:30", not to a key timeout with value 30. Your config.timeout is undefined, and nothing anywhere said so.
Implicit null. A key with nothing after it is not an empty string — it’s null. So are ~, null, Null, and NULL:
retries:
fallback: ~
cache: null
All three values are null. If you expected retries to be an empty string, a 0, or “unset means use the default,” you may instead be handing a literal null into code that doesn’t check for it. The value looks absent; it’s actually present and null.
The Norway problem and its friends. This is the one from the pillar, and it’s the headline silent failure. Under YAML 1.1 — still the default in parsers like PyYAML — a pile of bare words become booleans:
region: NO # the boolean false, not the country code "NO"
enabled: yes # the boolean true
debug: off # the boolean false
yes, no, on, off, y, n and their capitalizations all coerce to booleans. YAML 1.2’s core schema dropped this — there NO stays a string — but many runtimes still default to 1.1, so you can’t assume you’re safe. (The full story is in the pillar.)
Numbers that eat your strings. Anything shaped like a number becomes one, dropping whatever made it meaningful as text:
version: 1.10 # the number 1.1 — the trailing zero is gone
build: 1e5 # the number 100000 — read as scientific notation
zip: 01234 # 1234 (1.2 core) or 668 (octal, 1.1) — the leading zero vanishes
port: 0700 # 700 (1.2 core) or 448 (octal, 1.1)
time: 22:22 # 1342 under 1.1 (base-60: 22×60 + 22); a string under 1.2 core
Whether each of these coerces — and into what — depends on the parser and the schema it applies; under one that uses these implicit rules, they change type with no error at all. (22:22, for instance, stays a string under YAML 1.2’s core schema but becomes 1342 under 1.1.) A version check that expected "1.10" compares against 1.1 and disagrees; a zero-padded ID silently loses its zero.
Under a parser that applies these implicit rules, none of this usually errors, so there’s no line to jump to — as far as the parser is concerned, nothing went wrong. The debugging move can’t be “follow the error,” because there is no error. It has to be a habit instead.
How to debug each kind
The whole game is telling the two apart, and the test is one question: did you get a parse error, or not?
- You got an error. It’s a loud failure. The message carries a line and column — go there and fix the syntax. Tabs become spaces, siblings line up, quotes and brackets close, duplicate keys get renamed. Ten minutes, tops.
- It parsed, but something downstream is wrong. It’s a silent failure, and there’s no line to follow. Don’t reread the whole file looking for a typo — instead, look at the value your program actually received and work backward. A
falsewhere you expected text, anullwhere you expected a key, a number where you expected a version string: each points straight at an unquoted scalar that got coerced. The fix is almost always the same one habit — quote anything a human reads as text: country codes, versions, IDs with leading zeros, times, anything in theyes/no/on/offfamily.
region: "NO"
version: "1.10"
zip: "01234"
Quoting costs nothing and switches off the guessing for that value.
There’s a faster way to see the silent ones before they ship. Paste the file into the YAML converter and read the checks panel: it parses as YAML 1.2 by default but specifically flags several of the traps above — the YAML 1.1 boolean trap (that unquoted no), version numbers about to collapse into floats, big integers past 2^53, and tabs used for indentation — called out with the values they’ll change. Converting the same file to JSON catches the rest: the types you see in the JSON output are the types your program will actually get, so an implicit null or a stringified timeout:30 shows up for what it is.
The checklist
Next time YAML “breaks,” start by asking which kind:
- Is there a parse error? If yes, it’s loud and safe — go to the line and column it names. Tabs → spaces, fix the indentation, close the quote or bracket, dedupe the keys.
- Did it parse but behave wrong? It’s silent. There’s no line to follow — inspect the value your program received and trace it back to the scalar that got coerced.
- Quote the ambiguous scalars. Country codes, versions, leading-zero IDs, times, and the
yes/no/on/offfamily. Quoting turns off YAML’s guessing for that value — the single most effective habit against silent failures. - Run it through the checks panel before it ships. The YAML converter surfaces the silent coercions up front, and a peek at the JSON conversion shows you the real types.
The one line to keep: the YAML errors that stop your build are the ones you’re lucky to get. They cost you minutes and point at themselves. It’s the YAML that parses without a word — and quietly hands your program a false, a null, or a rounded-off number — that costs you an afternoon. Learn to suspect the quiet success, not just the loud failure, and most of YAML’s haunting goes away.