skip to content

Brace Expansion Points

Where a double-brace token is actually expanded, what a token may contain, and what is left in the text when nothing answers it. Interviewers use it to see whether you can debug a bad URL.

part ofAPI & DB clientsoverview, primer and where to startread it →
on this pageshow

explore

questions

4

A Postman request goes out with the literal text {{baseUrl}} still in its URL — what happened?

level: juniorimportance: must knowfreq 70%

answer

  1. Substitution is allowed to decline
  2. Look at what the replacer returns
  3. Only scalar values pass the gate
  4. The matched text is its own fallback

basics

~20 s

Nothing supplied a usable value for that name. The substitutor replaces a double-brace match only when the lookup yields a string, number or boolean; otherwise it returns the matched text unchanged, so the token itself is sent.

solid answer

~40 s

A literal `{{baseUrl}}` on the wire is the substitutor reporting that it found nothing usable under the name `baseUrl`. Postman's SDK scans the text with a pattern that captures whatever sits between the braces, looks that captured string up, and writes the result into the text **only** when its type is a string, a number or a boolean. For anything else — a name nobody defined, an entry that is disabled, a value that is not a plain scalar — the replacement function returns the matched text itself, which is a replacement that changes nothing. The request is still sent; it just goes to a host literally called `{{baseUrl}}`. There is no error, no warning and no failed run: the only evidence is the strange URL and whatever the transport says about it.

code

javascript · 12 lines
javascript
const REGEX_EXTRACT_VARS = /{{([^{}]*?)}}/g;
const NATIVETYPES = { string: true, number: true, boolean: true };
const sources = { host: 'api.example.com' };

const resolved = 'https://{{host}}/{{missing}}'.replace(
  REGEX_EXTRACT_VARS,
  (match, token) => {
    const found = sources[token];
    return NATIVETYPES[typeof found] ? found : match;
  }
);
// resolved === 'https://api.example.com/{{missing}}'

go deeper

for a junior

Be ready to say that the token is sent as written rather than becoming blank, and that the name printed inside the braces tells you exactly which value was missing.

for a middle

Explain the replacer: it looks the captured name up, and writes the result only when it is a string, a number or a boolean, otherwise returning the matched text unchanged.

for a senior

Show how you diagnose it in a real run — nothing errors, so you work from the symptom back to the name, checking exact spelling, whether the entry is enabled, and what supplied it on the machine where it worked.

for a principal

Own the argument for making missing inputs loud: a mechanism that fails silently by design needs a check of your own before the requests fire, so a misconfigured run fails on the missing name rather than on a confusing transport error.

## The mechanism: a replacement that is allowed to decline Brace expansion in Postman lives in the **collection SDK** — the library the desktop app and the command-line runner both load — and not in either front end. The SDK wraps the text in a small tracking helper and runs it through a regular-expression replace using `Substitutor.REGEX_EXTRACT_VARS`, a global pattern that matches a double-brace span and **captures the characters between the braces** as the name. For every match, the SDK calls a **replacer function** with two arguments: the matched text and that captured name. The replacer does exactly three things: 1. **Find.** It asks the substitutor for the captured name. Which store answers is a separate subject; all that matters here is that the lookup either produces a value or produces nothing. 2. **Flatten.** If what came back is a function it is invoked, and if the result carries a `toString` it is turned into a string. 3. **Gate.** The result is written into the text **only if its JavaScript type is one of the three the SDK calls native** — `string`, `number` or `boolean`, the keys of `Substitutor.NATIVETYPES`. For anything else the replacer returns `match`, which is the original `{{...}}` text. Returning `match` is a replacement that replaces text with itself. The token leaves the pass exactly as it entered, nothing later in the pipeline strips it, and it travels to the server verbatim. ## Why literal, and not blank The design choice worth naming is that the failure is **visible rather than silent-and-empty**. Had the replacer returned an empty string, `https://{{baseUrl}}/orders` would become `https:///orders` and you would be debugging a malformed URL with no clue what produced it. Because the token survives whole, the broken request **carries the name of the thing that was missing**. The first move when you see one is therefore not "why is the URL wrong" but "who was supposed to define `baseUrl`". That also means an unanswered token is **not** an error condition. Nothing throws, no assertion fails, and the run does not stop. The request is dispatched with the token embedded, and what you actually see is a downstream symptom: a DNS or connection failure if the token was in the host, a 404 if it was in the path, or a server-side validation error if it was in the body. ## What actually makes the lookup fail | What arrives in the request | What the lookup did | |---|---| | `{{baseUrl}}` arrives whole | no source held an enabled entry under the name `baseUrl` | | `{{ baseUrl }}` arrives whole | the captured name **includes the spaces**; the lookup is exact and nothing is trimmed | | `{{baseUrl}}` arrives whole although the entry exists | the entry is disabled, so it does not answer | | `{{cfg}}` arrives whole although `cfg` has a value | the value is not a string, number or boolean, so the gate rejected it | The second row is the one that catches people repeatedly. `{{ baseUrl }}` looks like the same token, but the pattern captures the characters between the braces **as written**, and the lookup is a plain keyed read. A leading or trailing space makes it a different name, and no entry is stored under it. ## Reading the symptom in practice A short checklist, in the order that resolves the most cases fastest: - **Copy the name out of the token exactly** and compare it, character for character, against the entry you believe defines it — including case, spaces and any punctuation. - **Check that the entry is enabled.** A disabled entry sits in the file and is visible in the UI, but it does not answer, so the symptom is identical to a name that was never defined at all. - **Check the value's type.** A scalar is written; anything the gate does not recognise is declined and the token survives. - **Check what supplied the value on the machine that used to work.** A run that passes in one place and fails in another usually differs in what was handed to it, not in the collection. - **Do not look for an error message.** There is none; the absence of a complaint is exactly what this mechanism guarantees. ## The wider rule this is an instance of The same rule governs every text field the runtime resolves, not just the URL — headers, the body and the parts of an auth block are all run through the same replacer. So the identical symptom shows up as a header whose value is `{{token}}`, or a JSON body containing `"id": "{{orderId}}"` as a literal string. The diagnosis never changes: the substitutor was asked for that name, and what came back was not something it was willing to write. Once you have internalised "an unanswered token is returned as itself", every one of those symptoms reads the same way, and the debugging step is always to go and find who was meant to define the name printed inside the braces.

  • The request was still sent — why does an unanswered token not abort the run?
    Because declining to substitute is a normal outcome of the replacer, not an exception. It returns the matched text and the pass completes successfully, so nothing upstream ever learns that a name went unanswered. Failure surfaces only as a downstream symptom — a connection error, a 404, a rejected body — which is why the token is left visible in the request for you to read.
  • An entry with that exact name exists and is enabled, yet the token still arrives literal. What else would you check?
    The value's type. The replacer writes the result only when it is a string, a number or a boolean; anything else is declined and the token is returned unchanged. Also re-read the token character for character — `{{ name }}` captures the spaces as part of the name, and nothing trims them, so it is not the same lookup as `{{name}}`.

It behaves like a mail merge that leaves the placeholder printed on the page when the column is missing, instead of quietly printing a blank.

saying these in an interview costs you the question

  • Says an unanswered token becomes an empty string in the request
  • Claims the run fails or raises an error when a token is unanswered
  • Assumes braces with spaces inside resolve the same as without
  • Thinks a disabled entry still supplies its value to the text
  • Believes any value type is written, including objects and null
open as a page

In a Postman collection, how does a token like {{host-{{env}}}} resolve, and what may a name never contain?

level: middleimportance: should knowfreq 42%

basics

~20 s

Innermost first. The extraction pattern excludes braces from a captured name, so only the inner token matches on the first pass; substitution then repeats and the composed name resolves later. A name may never contain a brace.

open as a page

In a Postman script, what does pm.variables.replaceIn('{{base}}/orders/{{id}}') return, and when would you use it?

level: middleimportance: should knowfreq 32%

basics

~20 s

The template with every token it can answer already replaced, and any it cannot left literal. It runs the same expansion the runtime runs over a request, on a string the script hands it, and returns the expanded result.

open as a page

At what point in a Postman or Newman run is a {{token}} in the request URL actually expanded?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Immediately before the send. The runtime resolves the item and its auth in the request step, after the pre-request script has finished, so a value the script has just written is already visible to the expansion.

open as a page