The Vibe Coding Delusion

Published: (December 15, 2025 at 08:47 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

Introduction

I recently sat in a code review that terrified me. A junior engineer—bright, enthusiastic, well‑meaning—had just vibe coded a new feature. He described the “vibe” of the feature to an LLM, pasted the output into our repository, and opened a PR. It worked: the pixels were in the right place, the button clicked, the data loaded.

But the file contained hard‑coded timeout values, duplicated state logic across three components, and a useEffect hook with a wildly permissive dependency array. It was a “happy path” masterpiece that would explode in production.

The promise of the current AI wave is that natural language is the new programming language. You tell the computer what you want; it handles the how. This is dangerous because the how is where bugs and security vulnerabilities live.

Vibe‑coded example

// UserProfile.jsx (The Vibe Version)
import React, { useState, useEffect } from 'react';

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [isEditing, setIsEditing] = useState(false);
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  useEffect(() => {
    fetch(`https://api.example.com/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setName(data.name);
        setEmail(data.email);
      });
  }, [userId]);

  const handleSave = () => {
    fetch(`https://api.example.com/users/${userId}`, {
      method: 'PUT',
      body: JSON.stringify({ name, email }),
      headers: { 'Content-Type': 'application/json' }
    }).then(() => {
      setIsEditing(false);
      // refetch to update
      fetch(`https://api.example.com/users/${userId}`)
        .then(res => res.json())
        .then(data => setUser(data));
    });
  };

  if (!user) return Loading...;

  return (
    
      {isEditing ? (
        
           setName(e.target.value)} />
           setEmail(e.target.value)} />
          Save
        
      ) : (
        
          
## {user.name}

          
{user.email}

           setIsEditing(true)}>Edit
        
      )}
    
  );
};

export default UserProfile;

For a junior developer this looks clean and works. For a senior engineer it raises red flags.

Why the Vibe code is problematic

Race condition

The useEffect fires a fetch each time userId changes. Rapid changes can cause out‑of‑order responses, displaying the wrong user data. The AI does not automatically use AbortController to cancel stale requests.

State desynchronization

Separate local states (name, email) are manually synced with user. If the parent updates user, the local fields may become stale, leading to bugs.

Missing error handling

No .catch() on the fetch promises. If the API is down, the component may crash or display nothing, because the AI assumes a happy path.

Hard‑coded fragility

URLs are hard‑coded, and there is no loading state for the save action. Multiple rapid clicks can fire duplicate PUT requests.

Security oversight

No authorization headers are sent. The component would silently fail with a 401 in a real application.

Specification Engineering approach

Define types and contracts

// types.ts
export type User = {
  id: string;
  name: string;
  email: string;
  updatedAt: string;
};

export type UserProfileProps = {
  userId: string;
  onUpdate?: (user: User) => void;
  // Dependency injection for fetching prevents hardcoded URLs
  fetchUser: (id: string, signal: AbortSignal) => Promise;
  updateUser: (id: string, data: Partial) => Promise;
};

Prompt with explicit constraints

Prompt:
Generate a UserProfile component based on the UserProfileProps interface above.
Constraints:

  • Use useQuery (or a similar pattern) to handle loading and error states explicitly.
  • Handle race conditions with AbortSignal.
  • Show a loading state on the Save button to prevent double‑submission.
  • Do not duplicate state; treat server data as the source of truth.
  • Display user‑friendly error UI instead of console logs.

Engineered version

// UserProfile.tsx (The Engineered Version)
import React, { useState, useEffect } from 'react';
import { User, UserProfileProps } from './types';

// Utility for safe state management
type FetchState = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

export const UserProfile = ({
  userId,
  fetchUser,
  updateUser,
  onUpdate,
}: UserProfileProps) => {
  const [state, setState] = useState>({ status: 'idle' });
  const [isSaving, setIsSaving] = useState(false);
  const [editForm, setEditForm] = useState | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    setState({ status: 'loading' });

    fetchUser(userId, controller.signal)
      .then(data => setState({ status: 'success', data }))
      .catch(error => {
        if (error.name !== 'AbortError') {
          setState({ status: 'error', error });
        }
      });

    return () => controller.abort();
  }, [userId, fetchUser]);

  const handleSave = async () => {
    if (!editForm || state.status !== 'success') return;

    try {
      setIsSaving(true);
      const updated = await updateUser(userId, editForm);
      setState({ status: 'success', data: updated });
      setEditForm(null); // Exit edit mode
      onUpdate?.(updated);
    } catch (e) {
      alert('Failed to save'); // Ideally use a toast system here
    } finally {
      setIsSaving(false);
    }
  };

  if (state.status === 'loading') return Loading...;
  if (state.status === 'error')
    return Error: {state.error.message};

  const user = state.status === 'success' ? state.data : null;

  return (
    
      {editForm ? (
        
           setEditForm({ ...editForm, name: e.target.value })}
          />
           setEditForm({ ...editForm, email: e.target.value })}
          />
          
            {isSaving ? 'Saving…' : 'Save'}
          
        
      ) : (
        user && (
          
            
## {user.name}

            
{user.email}

             setEditForm({ name: user.name, email: user.email })}>
              Edit
            
          
        )
      )}
    
  );
};

Takeaway

The solution isn’t to stop using AI; it’s to change how you use it.

  1. Write clear specifications, types, and constraints before prompting.
  2. Treat AI‑generated snippets as drafts that must be reviewed, tested, and hardened for production.
  3. Keep the “how” explicit—error handling, race‑condition mitigation, security, and state management—so the resulting code is reliable, maintainable, and safe.
Back to Blog

Related posts

Read more »