CodeCraftingHub
Home

Editor

Write, edit & format code with syntax highlighting

Sandbox

Run & test code in isolated environments

About UsToolsBlogs
Log in

CodeCraftingHub

Build, validate, and analyze code in one place with an engineering-first workflow.

Core

  • Workspace
  • Complexity analysis
  • Code tools (roadmap)

Elsewhere

  • GitHub
  • LinkedIn
  • Blogs

Site

About

© 2026 CodeCrafting. Execution + AI conversion modules are on the roadmap.

HomeAboutToolsEditorSandboxBlogsLog in
CodeCraftingHub
Home

Editor

Write, edit & format code with syntax highlighting

Sandbox

Run & test code in isolated environments

About UsToolsBlogs
Log in

CodeCraftingHub

Build, validate, and analyze code in one place with an engineering-first workflow.

Core

  • Workspace
  • Complexity analysis
  • Code tools (roadmap)

Elsewhere

  • GitHub
  • LinkedIn
  • Blogs

Site

About

© 2026 CodeCrafting. Execution + AI conversion modules are on the roadmap.

HomeAboutToolsEditorSandboxBlogsLog in
CodeCraftingHub
Home

Editor

Write, edit & format code with syntax highlighting

Sandbox

Run & test code in isolated environments

About UsToolsBlogs
Log in
vibe coding
Back to Articles

CodeCraftinghub

Coding

The 5-Second Vibe Check That Saves Me Hours of Debugging AI-Generated Code

By Usman AliApril 23, 20266 min read

Before you commit AI code, run a 5 second vibe check: Can you explain why it's structured that way? Is the state management sensible? Does it look like a 2021 tutorial? Three quick questions that save hours of debugging later.

I have a confession. I‘ve shipped AI written code that I didn’t fully understand. It worked in the browser, passed the quick smoke test, and looked clean enough. Then two weeks later, I‘m staring at a production bug that makes zero sense, and the culprit is that one block of code I accepted without a second thought.

It’s a terrible feeling. And I know I'm not alone.

After enough of these self-inflicted fire drills, I built myself a tiny habit that changed everything. I call it the Vibe Check. It takes five seconds literally and it's saved me from merging things that would have cost me hours (or days) down the road.

Here's exactly how it works, with real examples so you can steal it wholesale.

The Problem with AI Code: It Always Looks Fine

AI assistants like Cursor and Copilot are brilliant at generating code that compiles. The syntax is correct. The patterns are recognizable. The function names make sense. But here's the trap: correct syntax doesn't mean correct logic, and familiar patterns don't mean the right pattern for your codebase.

The AI doesn't know your app's data flow. It doesn't know your team's conventions. It doesn't know that the fetch Users function returns a cached promise and you shouldn't be calling it inside a useEffect like it's 2019.

But the code looks fine. So we merge it. And then we pay for it later.

The Vibe Check: Three Questions Before You Commit

Before I commit any chunk of code I didn't write entirely myself, I pause and ask myself these three questions. If any answer is a "no" or a "hmm," the code doesn't go in until I fix it.

1. The "Why" Test

Can I explain why this code is structured this way, in one sentence, without pointing at the screen and shrugging?

This sounds almost too simple, but it's the most powerful gate. If I can't articulate the reasoning behind the code, I don't understand it. And if I don't understand it, I can't maintain it.

Real example: I recently had AI generate a custom hook for managing a multi-step form state. It looked like this:

javascript
function useMultiStepForm(steps) {
  const [currentStep, setCurrentStep] = useState(0);
  const [formData, setFormData] = useState({});

  const nextStep = useCallback(() => {
    setCurrentStep((prev) => Math.min(prev + 1, steps.length - 1));
  }, [steps.length]);

  const updateField = (field, value) => {
    setFormData((prev) => ({ ...prev, [field]: value }));
  };

  return { currentStep, formData, nextStep, updateField };
}

Looks reasonable, right? But when I did the "Why" test, I realized I couldn't explain why nextStep was wrapped in useCallback but updateField wasn't. The AI had thrown in useCallback semi-randomly. There was no real performance concern. The dependency on steps.length was unnecessary because steps never changed after mount. A simpler version would have been:

javascript
function useMultiStepForm(steps) {
  const [currentStep, setCurrentStep] = useState(0);
  const [formData, setFormData] = useState({});

  const nextStep = () => {
    setCurrentStep((prev) => Math.min(prev + 1, steps.length - 1));
  };

  const updateField = (field, value) => {
    setFormData((prev) => ({ ...prev, [field]: value }));
  };

  return { currentStep, formData, nextStep, updateField };
}

I removed the useCallback entirely. The hook is simpler, the reasoning is clear, and I can explain it to a teammate in one breath: "It tracks current step index plus a flat form data object, and provides plain functions to advance and update fields." That's the bar.

If you find yourself saying, "I think it's for performance?" or "The AI put it there," pause. Don't commit yet.

2. The State Inspection

Did the AI use useState where it should have used a ref? Did it shove server state into useEffect instead of using a proper data-fetching library?

AI models have an almost romantic attachment to useEffect. It's their comfort blanket. If they need data, they reach for useEffect with an empty dependency array, fetch inside it, set some local state, and call it a day. This is fine for a tiny demo. It's a nightmare in a real app where you need caching, retries, deduplication, and stale while revalidate logic.

I've seen AI write stuff like this more times than I can count:

javascript
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch('/api/products')
    .then((res) => res.json())
    .then((data) => {
      setProducts(data);
      setLoading(false);
    });
}, []);

This breaks the second you navigate away and come back. It refetches unconditionally. It doesn't cache. It doesn't handle errors gracefully. In my codebase, we use TanStack Query (React Query) for this. The AI didn't know thatunless I told it. The fix:

javascript
const { data: products, isLoading } = useQuery({
  queryKey: ['products'],
  queryFn: () => fetch('/api/products').then((res) => res.json()),
});

Now I get caching, background refetching, error boundaries, and a loading state that just works. The AI's version was a time bomb.

The other common state sin is using useState for values that don't need to trigger re-renders. I once caught AI generating a useState for a timer ID that was only ever read inside a useEffect cleanup function. That should have been a useRef. Re-renders on timer updates? No thanks.

Rule of thumb: If changing the value shouldn't cause a visual update, lean toward a ref. If the value comes from a server, use a library designed for it—not raw useEffect.

3. The Freshness Check

Does this look like it was copied from a Medium article titled "Top 10 React Hooks for 2021"?

AI models were trained on code from the past. Sometimes the distant past. I've seen AI generate class components in a hooks-only codebase. I've seen componentDidMount-style logic smuggled into functional components via bloated useEffect blocks. I've seen var instead of let/const in a TypeScript project. (That one was almost impressive in its wrongness.)

Real example from a PR I reviewed last month:

javascript
useEffect(() => {
  // Runs once on mount (like componentDidMount)
  loadUserPreferences();
  const interval = setInterval(() => {
    refreshToken();
  }, 60000);
  return () => {
    clearInterval(interval);
  };
}, []);

This works. But it's a 2019 pattern. The comment even admits the mental model: "like componentDidMount." In 2026, that interval logic should probably live in a custom hook or an external store, not jammed into a useEffect that has nothing to do with rendering. And if the data is server state, we're back to TanStack Query's refetchInterval.

The freshness check is simple: If you squint and can picture a class component behind this code, it's probably outdated. React moved on. Hooks are the standard. Server Components exist. We have better patterns now. The AI doesn't always know that unless you shepherd it.

Putting It Together: A Real Vibe Check in Action

Here's a realistic scenario from my workflow. I ask Cursor for a component that displays a user's profile and lets them toggle between editing and viewing mode. I'm tired and a little lazy, so I accept the whole block without tweaking the prompt much.

What I get back:

javascript
const ProfileCard = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [isEditing, setIsEditing] = useState(false);
  const [formData, setFormData] = useState({});
  const inputRef = useState(null); // ❌ Wrong—should be useRef

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        setUser(data);
        setFormData(data);
      });
  }, [userId]);

  const handleSave = async () => {
    await fetch(`/api/users/${userId}`, {
      method: 'PUT',
      body: JSON.stringify(formData),
    });
    setIsEditing(false);
  };

  if (!user) return <p>Loading...</p>;

  return (
    <div>
      {isEditing ? (
        <form onSubmit={handleSave}>
          <input
            value={formData.name}
            onChange={(e) => setFormData({ ...formData, name: e.target.value })}
          />
          <button type="submit">Save</button>
        </form>
      ) : (
        <div>
          <h2>{user.name}</h2>
          <button onClick={() => setIsEditing(true)}>Edit</button>
        </div>
      )}
    </div>
  );
};

Let's run the Vibe Check.

1. The "Why" Test: Can I explain why useEffect fetches on userId change? Yes that part is clear. But why is inputRef declared as a state? I can't explain that. It should be a ref. Why is form data duplicated in both user and formData state? That's muddy. Not a clean pass.

2. The State Inspection: Fetching server state in useEffect classic. No caching, no error handling, no optimistic updates. Should be TanStack Query. Also, inputRef as state is a state misuse. Should be useRef (though ironically the generated code doesn't even use it anywhere, so it's dead code too).

3. The Freshness Check: The pattern is fundamentally functional, but that useEffect fetch pattern feels pre React Query. The inputRef state gaffe smells like an older mental model leak. Not fresh.

This code would technically run. A quick smoke test would pass. But in production, it's fragile and full of small landmines. The Vibe Check caught it in five seconds. I rejected the chunk, wrote a better prompt referencing our existing hooks, and got this instead:

javascript
const ProfileCard = ({ userId }) => {
  const { data: user, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then((res) => res.json()),
  });
  const [isEditing, setIsEditing] = useState(false);
  const [name, setName] = useState(user?.name ?? '');

  const updateUser = useMutation({
    mutationFn: (data) =>
      fetch(`/api/users/${userId}`, {
        method: 'PUT',
        body: JSON.stringify(data),
      }),
    onSuccess: () => setIsEditing(false),
  });

  if (isLoading) return <p>Loading...</p>;

  return (
    <div>
      {isEditing ? (
        <form onSubmit={(e) => { e.preventDefault(); updateUser.mutate({ name }); }}>
          <input value={name} onChange={(e) => setName(e.target.value)} />
          <button type="submit">Save</button>
        </form>
      ) : (
        <div>
          <h2>{user.name}</h2>
          <button onClick={() => setIsEditing(true)}>Edit</button>
        </div>
      )}
    </div>
  );
};

Cleaner. Uses the data layer we actually use. Mutations handled properly. The Vibe Check takes no time at all, and it turns AI scaffolding into solid code.

This Isn't About Distrusting AI

I'm not telling you to stop using AI. I use it every day. The Vibe Check isn't about paranoia it's about ownership. The code that lands in your repo has your name on it. The AI doesn't get paged at 3 a.m. when something breaks. You do.

By spending five seconds on these three questions, I catch most of the nonsense before it ever reaches a branch. The rest I catch in a proper code review, which is a topic for another day.

Try the Vibe Check on your next AI-aided PR. See how many "looks fine" moments it turns into "wait, let me fix that." I bet it's more than you think.

  • The Problem with AI Code: It Always Looks Fine
  • The Vibe Check: Three Questions Before You Commit
  • 1. The "Why" Test
  • 2. The State Inspection

Comments

No approved comments yet.

Related Articles

The Myth of Comments: Why Self‑Explanatory Code Wins
Coding

The Myth of Comments: Why Self‑Explanatory Code Wins

The Art of the Silent Codebase: When to Speak and When to Code Every developer has seen it: a helpful comment that lies. The code was refactored, the comment remained, and now it actively misleads the next engineer. The root cause is a fundamental truth comments and code live separate lives. When you change code, you rarely remember to update the comment. Over time, this creates a silent swamp of outdated, even dangerous, documentation. The solution isn’t to ban comments. It’s to treat them as the exception, not the rule. By writing self explanatory code, you eliminate the need for most comments entirely. Your code becomes the single source of truth clean, expressive, and impossible to go out of sync. In this article, we’ll explore how to write professional, self‑documenting JavaScript. You’ll see concrete examples that make comments redundant and learn patterns that keep your codebase maintainable. The Cost of Comment‑Dependent Code Consider this typical snippet: At first glance, the comments seem helpful. But what happens when the discount logic changes to a tiered system? Or the tax rate becomes dynamic? The comments will almost certainly stay as they are, creating a trap for anyone who trusts them. Worse, the code itself is noisy with boilerplate and magic numbers. Now let’s rewrite it without a single explanatory comment, using only expressive naming and structure. Self Explanatory Code in Action 1. Use Descriptive Names Names are the most powerful tool for self‑documentation. A well‑chosen function or variable name tells you what and why. What changed? No comments needed. The function names (calculateSubtotal, applyDiscount, addTax) are mini‑documents. Constants replace magic numbers. Each function does one thing and has a clear, testable boundary. 2. Embrace Small, Pure Functions When a function does only one thing and its name describes that thing perfectly, comments become unnecessary. Now the code reads like a story. You don’t need a comment to understand the permission logic the function names and composition tell you everything. 3. Use Modern JavaScript to Express Intent Destructuring, default parameters, and object shorthand can turn cryptic code into clear declarations. The destructured parameter makes it obvious what inputs are expected. The default value for role is right where you need it. The code is compact yet perfectly clear. 4. Avoid “What” Comments Let the Code Speak Comments that restate what the code does are noise. The code itself can and should—do that. When you use higher‑order functions like reduce, map, or filter, the intent is embedded in the method name. You no longer need comments to explain iteration. 5. Use Meaningful Constants for Business Logic Magic numbers are a common source of “what” comments. Turn them into named constants. Now the condition reads like a business rule. Any future change to the threshold is isolated to the constant, and the function name tells you exactly what’s being checked. When Do You Use Comments? Self‑explanatory code doesn’t mean never write comments. It means using comments for things code cannot express: Why a certain approach was taken (a trade‑off, a workaround for a bug in a dependency). Complex business rules that are not obvious from the code alone. Public API documentation (JSDoc) to describe parameters and return types, especially in libraries. Example of a good comment: Conclusion Comments are not evil, but they are a liability when used as a crutch for unclear code. Every time you write a comment, ask yourself: Can I rewrite the code so this comment becomes unnecessary? Professional engineers know that code is read far more often than it is written. By investing in self‑explanatory code clear names, small functions, meaningful constants, and expressive modern syntax you build a codebase that is a joy to read, safe to change, and immune to the silent rot of outdated comments. The next time you’re about to add a comment, let the code speak for itself. Your future self (and your teammates) will thank you. Happy coding, and may your code always be its own best documentation.

Want More Engineering Deep Dives?

Join the newsletter for practical insights on architecture, code quality, and developer workflow.

CodeCraftingHub

Build, validate, and analyze code in one place with an engineering-first workflow.

Core

  • Workspace
  • Complexity analysis
  • Code tools (roadmap)

Elsewhere

  • GitHub
  • LinkedIn
  • Blogs

Site

About

© 2026 CodeCrafting. Execution + AI conversion modules are on the roadmap.

HomeAboutToolsEditorSandboxBlogsLog in
  • 3. The Freshness Check
  • Putting It Together: A Real Vibe Check in Action
  • This Isn't About Distrusting AI
  • code-editor
    Coding

    Free Online Code Editor No Sign Up: Start Coding Instantly with CodeCraftingHub

    CodeCraftingHub is a free online code editor and learning platform for front-end developers. Write, run, and share code instantly. Learn from articles, structured paths, and hands-on challenges. No account required to get started.

    Coding
    Coding

    Writing Secure Code in an AI-Assisted World: Pitfalls and Good Practices

    AI writes fast. It doesn't write secure. Hardcoded secrets, injection flaws, and over-privileged logic slip in constantly because models solve tasks, not threat models. Here's the practical checklist to own your code not just mop up after the robot.