I Ran Nark Against My Own Codebase. Here's Every Violation It Found.
By Caleb Gates
I built Nark to catch the errors that AI-generated code misses. Then I ran it against the codebase that powers nark.sh itself — a Next.js app with Clerk auth, Stripe billing, and a Postgres backend. The dashboard you'd use to view your scan results was, itself, full of violations.
11 were real bugs. 122 were false positives. Both numbers taught me something.
The Setup
The nark.sh dashboard is a Next.js 14 App Router project. It uses date-fns for formatting, undici under the hood for fetch calls, Clerk for authentication, and Next.js redirect() and notFound() throughout. It's the kind of codebase that accumulates quickly when you're building with Claude Code and shipping fast.
I ran the scan the same way any user would:
npx nark --tsconfig ./tsconfig.json
133 violations came back. My own scanner gave my own code a failing score.
What Was Actually Broken
date-fns: format() without isValid() guards
Nark flagged every format() and formatDistanceToNow() call that didn't check whether the date was valid first. In my utility functions, I was parsing date strings and passing them straight to date-fns:
export function formatRelativeDate(date: Date | string): string {
const dateObj = typeof date === "string" ? new Date(date) : date;
const daysDiff = differenceInDays(now, dateObj);
// ...
}
If date is "not-a-date" or null coerced to a string, new Date(date) produces an Invalid Date object. Passing that to format() throws a RangeError. The fix:
import { isValid } from "date-fns";
export function formatRelativeDate(date: Date | string): string {
const dateObj = typeof date === "string" ? new Date(date) : date;
if (!isValid(dateObj)) return "unknown";
// ...
}
One line. Would I have found this without Nark? Probably not until a user hit it with a malformed timestamp from the API.
undici: response.json() outside try-catch
One fetch call in an admin panel was calling res.json() without a try-catch. If the server returned non-JSON (an HTML error page, a 502 from the load balancer), the call would throw an unhandled error and crash the component:
// Before: no protection against non-JSON responses
const json = await res.json();
setItems((prev) => [...prev, json.data]);
// After: handle parse failures gracefully
let json: { data: Followup };
try {
json = await res.json();
} catch {
console.error("Failed to parse followup response");
setAdding(false);
return;
}
setItems((prev) => [...prev, json.data]);
This is the kind of bug that only surfaces when your infrastructure hiccups. A load balancer returning a 502 HTML page instead of JSON. A CDN edge timing out. You never see it in development.
Clerk: missing isLoaded guards
Three layout components were accessing useUser() data without checking isLoaded first. Clerk's docs are clear: the user object is undefined until the auth state loads. Without the guard, you get a flash of incorrect UI or a runtime error on slow connections:
// Before
const { user } = useUser();
// After
const { user, isLoaded } = useUser();
if (!isLoaded) return <Skeleton />;
Next.js redirect() inside a catch block
This one was subtle. My admin layout had a pattern like this:
try {
const isAdmin = await checkAdmin();
if (!isAdmin) redirect("/dashboard");
} catch (error) {
// Handle auth errors
}
The problem: Next.js redirect() works by throwing a NEXT_REDIRECT exception. Wrapping it in try-catch means the catch block swallows the redirect and navigation never happens. The fix was to move the redirect outside the try-catch:
const isAdmin = await checkAdmin().catch(() => false);
if (!isAdmin) redirect("/dashboard");
Nark's Next.js profile knows that redirect() throws intentionally. It flagged the pattern. This was a real bug that would have silently broken admin access checks.
What Was Not Broken (The 122 False Positives)
Here's where it gets interesting. Most of what Nark flagged was wrong. And the categories of "wrong" told me more about my scanner than the real bugs did.
70 undici false positives: scanner can't see surrounding try-catch
The biggest category. Nark flagged response.json() calls as unprotected, but they were inside try-catch blocks. The scanner was looking for try-catch immediately wrapping the call site, but in many cases the try-catch was a few lines up in the function scope:
try {
const res = await fetch("/api/something");
if (!res.ok) throw new Error("Failed");
const data = await res.json(); // Nark flags this — but it's inside try-catch
return data;
} catch (error) {
toast.error("Something went wrong");
}
The scanner's AST walk wasn't climbing high enough to find the enclosing try-catch. This is a scanner bug, not a code bug.
49 Next.js false positives: redirect/notFound are intentional throws
Next.js uses exceptions for control flow. redirect() throws NEXT_REDIRECT. notFound() throws NEXT_NOT_FOUND. The framework's error boundary catches these. Wrapping them in try-catch would break them.
Nark's Next.js profile had postconditions like "redirect must be inside try-catch" — which is correct for most functions that throw, but wrong for Next.js control-flow exceptions. The scanner was applying generic error-handling rules to a framework that deliberately violates them.
9 date-fns false positives: already guarded by isValid()
After I added the isValid() guards, the remaining date-fns violations were in files where the guard was already present. The scanner wasn't tracing the control flow from the guard to the format call.
The Suppression Workflow
False positives are inevitable in static analysis. The question is what you do with them. Nark uses a .nark/suppressions.json file in the project. Each entry targets a specific package and postcondition, with a reason string:
{
"ignore": [
{
"package": "next",
"postconditionId": "redirect-inside-try-catch",
"reason": "Next.js redirect() intentionally throws a NEXT_REDIRECT exception caught by the framework error boundary. Wrapping in try-catch would break navigation."
},
{
"package": "undici",
"postconditionId": "response-json-parse-error",
"reason": "All response.json() calls are inside try-catch blocks. Scanner does not detect surrounding try-catch."
}
]
}
This file is checked into version control. It's the project's explicit record of "we reviewed this, it's fine, here's why." Eight rules covered all 122 false positives. After suppression, the score hit 100/100.
The reason strings matter. They're not just comments. When the scanner improves and a suppression becomes unnecessary, the reason tells you whether to remove it or keep it. "Scanner can't see surrounding try-catch" is a scanner limitation that should get fixed. "Next.js redirect() intentionally throws" is a permanent architectural fact.
What Dogfooding Reveals About Scanner Quality
Running your own tool against your own code compresses the feedback loop to zero. Every false positive is something you personally have to deal with. Every real bug is one you personally shipped.
Here's what the numbers say about Nark's current accuracy on a real Next.js codebase:
| Category | Count | Verdict |
|---|---|---|
| Real bugs fixed | 11 | Code changes committed |
| Scanner can't see try-catch | 70 | Scanner improvement needed |
| Framework control-flow exceptions | 49 | Profile adjustment needed |
| Already guarded | 9 | Flow analysis improvement needed |
| Total | 133 | 8.3% true positive rate |
An 8.3% true positive rate is not good enough for a tool that's supposed to run in CI. If one in twelve flags is real and eleven are noise, developers will stop reading the output. That's the death of any static analysis tool.
But the 11 real findings were genuinely useful. The redirect() inside a catch block was a real auth bypass. The missing isValid() guards would have crashed on bad data. The res.json() without try-catch would have blown up on the first infrastructure hiccup.
The fix isn't to lower the bar. It's to make the scanner smarter about the specific patterns that generate false positives. Each category points to a concrete improvement:
- Try-catch detection needs to walk up the AST to enclosing scopes, not just check the immediate parent
- Framework-specific profiles need to mark control-flow exceptions as intentional throws
- Guard detection needs to trace
isValid()calls through to the format calls they protect
Every suppression reason in .nark/suppressions.json is a bug report against the scanner itself. I fed each one back into the improvement pipeline.
The Feedback Loop
Nark's dashboard has a "Report False Positive" button on every violation. When users (or in this case, me) click it, the report gets queued. A separate process pulls those reports and converts them into scanner upgrade tasks: adjust a profile, improve AST traversal, add a new pattern match.
The suppressions I wrote while fixing my own codebase generated eight scanner improvement tickets. Three of them — the try-catch scope detection, the Next.js control-flow exception handling, and the date-fns guard tracing — are already fixed in the latest release. The next time someone scans a Next.js app, those 49 redirect/notFound false positives won't fire.
This is the part of building a static analyzer that doesn't get talked about enough. The scanner is never done. Every real codebase teaches it something. The difference between a useful tool and an annoying one is how fast the false positive feedback converts into scanner improvements.
What I'd Tell Another Founder
If you build developer tools, scan your own code first. Not as a marketing exercise. As an engineering exercise.
You will find real bugs. That's satisfying but expected. The more valuable thing is the false positives. They show you exactly where your tool's model of the world doesn't match reality. Every FP is a gap between what your tool thinks code looks like and what code actually looks like in production.
The specific things I learned:
- Framework conventions break generic rules. Next.js uses exceptions for control flow. Your "always wrap in try-catch" rule is wrong in that context. You need framework-aware profiles.
- Try-catch scope matters. Looking only at the immediate parent node misses most real-world error handling. Functions wrap their entire body in try-catch, not individual lines.
- Guard clauses are invisible to naive AST analysis. An
isValid()check three lines before aformat()call is just as good as a try-catch. The scanner needs to understand data flow, not just syntax. - Suppression UX determines whether the tool gets adopted. If suppressing a false positive takes more effort than fixing the "bug," developers will just turn off the tool. Config-level suppressions (one rule covers 70 violations) beat per-line annotations.
The 11 real bugs are already fixed. The 122 false positives made the scanner better for everyone. That's the trade.
Try It on Your Codebase
npx nark --tsconfig ./tsconfig.json
Nark checks 160+ npm packages — including axios, prisma, stripe, and redis — for unhandled error paths, missing guards, and incorrect usage patterns. If you find false positives, report them. They make the scanner better.
To work through violations interactively with Claude Code, check out nark-fix. It pulls your scan results via MCP and walks through each violation with you — fix, suppress, or report as false positive. The scanner itself is open source at github.com/nark-sh/nark.