Edge cases and the bugs you would have shipped
The bugs that reach production almost never live in the happy path. Two real tests, from a real code review, show what actually catching them looks like.

Nobody's login form breaks on a normal email and a normal password. It breaks on the email that's already taken, the password that's one character short, the request that arrives twice in the same millisecond. The happy path is the part everyone tests without thinking about it. The edges are where the real bugs live, and they're exactly the cases it's easiest to forget.
A clamp bug you can see right now
Start small. Here's a naive pagination helper that clamps a limit into a safe range:
Run it. The second check fails, because clamp never applies the upper bound at all, only the lower one. And here's the trap: if the only test you'd written was clamp(20, 1, 100), that bug would have shipped with a fully green suite. The happy-path call never exercises the branch that's broken. We'll come back to exactly this shape of problem in a later lesson about what code coverage does and doesn't tell you.
Two edge cases that were real bugs
The rest of this lesson is quoted straight from auth.test.ts in Linkstash, the bookmark API from the Backend & APIs series. Both tests exist because a code review caught the bug they're now guarding against.
First: login timing shouldn't leak who has an account. A naive login check looks up the user by email, and if nobody matches, returns "unauthorized" immediately, skipping the password check entirely since there's nothing to compare against. That shortcut is faster for unknown emails than for real ones, and a timing difference that small is enough for an attacker running the check thousands of times to figure out which emails are registered, without ever guessing a password.
it("spends hashing work even when the account does not exist, so timing cannot enumerate users", async () => {
const spy = vi.spyOn(argon2, "verify");
expect(await verifyUser(db, "nobody@example.com", "correct horse battery")).toBeNull();
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});verifyUser still calls argon2.verify even for an email that was never registered, hashing a password against a dummy value that takes the same amount of work as a real check. The response takes roughly the same time either way, so timing alone can't tell an attacker which emails exist. vi.spyOn wraps the real argon2.verify and records every call without changing what it does, which is exactly what you want when you're proving something ran, not faking its result. We'll cover spies properly in the next lesson.
Second: two people registering the same email at the same instant should never both succeed, and neither should crash. Databases enforce a unique constraint on email, but two requests can both pass the "is this email taken?" check before either one finishes inserting, a classic race condition.
it("maps a concurrent duplicate registration to EmailTakenError, not a raw driver error", async () => {
const results = await Promise.allSettled([
registerUser(db, "diya@example.com", "correct horse battery"),
registerUser(db, "diya@example.com", "correct horse battery"),
]);
expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(1);
const rejected = results.filter((r) => r.status === "rejected") as PromiseRejectedResult[];
expect(rejected).toHaveLength(1);
expect(rejected[0]!.reason).toBeInstanceOf(EmailTakenError);
});Promise.allSettled fires both registrations at once and waits for both to finish, pass or fail, instead of stopping at the first rejection the way Promise.all would. The test proves exactly one registration wins and the loser gets a clean EmailTakenError, the same error a normal duplicate-email request would get, not a raw SQLite constraint violation with a driver-specific message leaking into an API response. Without this test, that race condition is invisible in manual testing. You'd have to click "register" twice in the same millisecond to ever see it, which nobody does by hand.
Quick check
Why does verifyUser() still call argon2.verify() even when no user matches the email?
Where to actually look for edge cases
You won't spot every one of these by staring at a function. A short checklist that catches most of them: zero and negative numbers where you expected positive, empty arrays and empty strings, two operations happening at the same time instead of one after another, and anyone accessing something that belongs to someone else. That last one is exactly why Linkstash returns a 404, not a 403, when you try to fetch another user's link, a decision the error-handling lesson in the Backend series covers in more depth.
Previous: TDD: red, green, refactor. Next: mocks, stubs and fakes, where vi.spyOn gets the full explanation it deserves.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


