Skip to content

Build1 publisher2 min readPublished

Exiting zero on a failed FX fetch keeps yesterday's rates in the build

In a Next.js storefront selling into 32 European markets, exchange rates come from one prebuild request to frankfurter.app, and a failed fetch leaves the last committed JSON in place with exit code zero.

The Engineer · Build desk

What happened

  • A prebuild script fetches EUR-to-X for ten non-euro currencies in a single request to frankfurter.app, which mirrors the European Central Bank's daily reference rates and needs no API key.
  • The result is written to src/data/fx-rates.json with a generatedAt timestamp, an EUR base and a note reading "Approximate reference only. Checkout charges in EUR.", and the file is committed to the repo.
  • On any failure the catch block logs the error, logs that it is keeping the committed JSON as-is, and calls process.exit(0), so the shell and CI record the prebuild step as a success.
  • The author's reason for not throwing is that every deploy runs the build, so a transient 503 from the rates API would block hotfixes that have nothing to do with currency.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint No build ever fails when the rates stop refreshing, so detecting staleness has to live outside the pipeline, in whatever process reads the generatedAt field.
  • exposure A wrong-but-numeric rate from upstream reaches shoppers with the same green build as a correct one, and the first person likely to notice is a UK buyer comparing the hint against their bank.
  • cost Committing the artifact means the repo absorbs a rate diff on most builds, and reviewers pay for it in churn they are expected to wave through.
  • precedent Once one prebuild fetch is allowed to report success while failing, the next external data step added beside it inherits that reading by default, whether or not its output is display-only.

Both validation throws sit inside `fetchRates`, and the single `.catch` at the bottom of the file receives them [10][11]. So a 503 from frankfurter.app, a rate-limited response, a malformed payload and a missing GBP key all land in the same handler with the same exit code [19]. The whole alarm surface is two `console.error` lines [11].

What makes that proportionate is the job the number has. The JSON feeds one element, an approximate hint beside a euro price, "≈ £25" next to "€29" [12]. Checkout charges in EUR whichever country the buyer is in [12]. The artifact says so itself, in a note committed next to the rates: "Approximate reference only. Checkout charges in EUR." [9]. The author wrote that "Shipping a build with FX numbers a day, or a week, old is the cheaper failure" [14], and the fallback is a day-old real rate from the same source, because the committed file was itself fetched from frankfurter.app at an earlier build [15].

Build-time caching does not remove the dependency on frankfurter.app. It moves the failure to a point where the last good value is already on disk [3][15]. In this design there was no third-party call on the payment path to remove, because the conversion never touches the charged amount [12]. Charge in local currency and the same handler ships yesterday's rate as today's price under a passing build [11][12]. The post compares the choice to retrying a webhook call versus failing it outright, and asks what the failure actually costs [21].

The committed file drifts too. A hardcoded rates object drifts silently because nothing breaks when a number is merely wrong [2]; the JSON at least carries a `generatedAt` timestamp [9]. Nothing in the script compares that timestamp to the current date, and nothing bounds the values: `typeof r !== "number"` rejects a missing key or a string, and accepts 0.086 for GBP as readily as 0.86 [10][20].

The post's headline offers VAT and FX rates at build time [17]. The script covers FX, and the write-up points to a project card for the broader VAT and payments architecture without showing how the tax tables are fetched [17].

The storefront ships to 32 markets and targets ten non-euro currencies, so in 22 of them the displayed and the charged number are the same, if each of those ten currencies maps to one market [18]. The prebuild hook fires only on `npm run build`, and `npm run dev` never invokes it, so local development serves whatever rates happen to be committed [16].

What to watch

  • frankfurter.app needs no API key today; if it adds one or tightens rate limits, the build step gains a secret and a quota to manage.
  • What the app does on a first build when src/data/fx-rates.json is absent, since the failure path preserves a file instead of writing one.
  • Whether the per-build churn in the committed JSON ends up excluded from code review as the diffs accumulate.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories