Find out what won't compile
FSR turns your TypeScript into data a Rust host runs. Not every line survives that. You'll write code that doesn't, read what the build says about it and learn the two different things it can mean.
Before you start
Straight from crates.io. No Node, no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
Break a loader
A loader has to run in Rust, so it gets the strict treatment. Try reaching for the clock.
import type { Ctx } from "@snapfire/fsr";
export async function load(_ctx: Ctx<"/">) {
const stamp = new Date().toISOString();
return { greeting: "Hello from Rust", stamp };
}$ fsr build hello/app
routes/page.loader.ts:4:17: `new`
$ echo $?
1
That's the whole message. new isn't in the vocabulary a loader can use. You get the line and column, a non-zero exit and nothing written. You find out here instead of in a request.
Break a component
A component gets treated differently, because a component that can't be lowered still has somewhere to run.
import type { RootProps } from "@generated/client";
export default function Index({ greeting, code }: RootProps) {
return (
<section className="hero">
<h1>{greeting}</h1>
<p>Order {code.padStart(4, "0")}</p>
</section>
);
}$ fsr build hello/app
rendered routes/page.tsx#default client routes/page.tsx:7:17
client routes/page.tsx:7:17 `.padStart()`, which is not a builtin
the builtins are `map`, `filter`, `find`, `findIndex`, `some`, `every`,
`reduce`, `join`, `includes`, `trim`, `repeat`, `toFixed`, `toUpperCase`,
`toLowerCase`, `split`, `startsWith`, `endsWith`, `replace` and
`toLocaleString`; anything else goes in a module-level helper the build
can read
1 page renders in the browser for it
routes/page.tsx#default
$ echo $?
0
The build succeeded. Your page works. It just renders in the browser now instead of on the server. The report tells you exactly which line cost you that and what the alternatives are.
Why one is an error and one isn't
A loader refusal is an error because there's no fallback: the loader has to run in Rust or it doesn't run. A component refusal is a report line because the browser is a real place for a component to run. Sometimes it's where you wanted it anyway.
What makes the second one dangerous is that it's quiet. Nothing is broken, so nothing shouts. Read the client rows on every build and you will notice the day a page you cared about stopped being server-rendered.
One bad call spreads
This is the part that bites. One unlowerable call in a small leaf component marks that component client. The page importing it's then client too. A client-rendered page doesn't register its child islands, so the server island you built in 020 quietly stops being a server island.
One .padStart() in a badge component can turn a server-rendered page tree into a browser-rendered one. The report is how you catch it. Read the client rows, not just the green checkmark.
How to fix it
Two moves cover nearly everything.
Move the work into the loader. The loader already runs in Rust and has a wider vocabulary than a render path does, so formatting a value before it reaches the component is usually the right answer anyway.
Or put it in a module-level helper the build can read, which is what the report's last line is telling you. A named function at module scope gets lowered once and called from the render path.
No runtime surprise
Both refusals happen at build time and both name the line. Neither shows up as a runtime error.
Next up: 060. Run one app on two domains, where one binary answers for two brands.