powershelldba.de · Uwe Janke

The Problem with Pretty Code

Sloppy formatting used to be a free warning label. Cramped indentation, inconsistent casing, a variable named x2, an empty catch block sitting there in plain sight, all of it told an experienced reviewer exactly where to slow down. AI-generated code doesn't do that anymore. It's uniformly clean, consistently indented, and confidently named, whether or not the logic underneath is right. The tell is gone, and most review habits haven't caught up.

The Old Tell

Formatting quality and logical correctness used to travel together, not because messy syntax causes bugs, but because they shared a common cause: the author's attention. A developer who couldn't be bothered to align a WHERE clause or close a brace properly usually hadn't been bothered to think through the edge case three lines below it either. Reviewers learned this correlation the hard way, and it became a real, if informal, review heuristic: find the ugly block, read it twice.

It wasn't a perfect signal. Plenty of careful people write ugly code, and plenty of careless people write pretty code. But as a triage tool, applied across hundreds of lines under time pressure, "which part looks rushed" was a genuinely useful place to start looking for the part that was rushed.

What Changed

An AI assistant doesn't get tired, doesn't skip formatting when it's in a hurry, and doesn't leave a variable half-renamed because it ran out of patience halfway through a refactor. Every function gets the same consistent indentation, the same descriptive names, the same tidy error handling scaffolding, whether the code inside that scaffolding is correct or not. The visual signal that used to correlate with author attention now just reflects the formatter, and the formatter doesn't know whether the query is right.

That would be a minor inconvenience if pretty and ugly code were equally likely to be correct. They're not, in one specific and dangerous way: pretty code doesn't just fail to signal a bug, it can actively borrow the visual signature of the correct pattern while still containing the incorrect one. A well-formatted call to sp_executesql looks like the safe, parameterized way to run dynamic SQL, whether or not a parameter was actually used. A tidy try/catch with a logging line inside it looks like proper error handling, whether or not anything is actually done with the exception. The old heuristic didn't just go quiet, it started actively pointing reviewers away from the real problem.

Same Bug, Two Outfits

The three pairs below aren't hypothetical edge cases. Each one is a bug pattern that shows up regularly in real code, once written by a rushed human and now more often generated by an assistant that formats it beautifully. In every pair, the bug is identical. Only the packaging changed.

SQL: dynamic filtering

Obviously wrong — nobody ships this without a second look
declare @sql varchar(4000)
set @sql='select * from Orders where CustomerID=''' + @custId + ''' and Status=''' + @status + ''''
exec(@sql)
Pretty, generated, and still injectable
DECLARE @Sql NVARCHAR(MAX);

SET @Sql = N'
    SELECT  OrderId,
            OrderDate,
            Status
    FROM    dbo.Orders
    WHERE   CustomerId = ''' + @CustomerId + N'''
      AND   Status     = ''' + @Status + N''';';

EXEC sp_executesql @Sql;

The second version reads as careful T-SQL: proper casing, aligned columns, a fully qualified table name, and sp_executesql instead of a bare EXEC(), the exact procedure most style guides recommend precisely because it supports parameters. But no parameters were passed. The string is still built by concatenation, the injection is still there, and the choice of sp_executesql now works against the reviewer, because it pattern-matches on "the safe call" without anyone checking that it was used safely.

PowerShell: error handling

Obviously wrong — an empty catch block is a classic red flag
function Backup-Db{
param($db)
try{
Backup-SqlDatabase -Database $db -BackupFile "D:\Backup\$db.bak"
}catch{}
Write-Host "done"
}
Pretty, generated, and still swallowing the failure
function Backup-SqmDatabase {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Database
    )

    try {
        Backup-SqlDatabase -Database $Database -BackupFile "D:\Backup\$Database.bak"
        Write-Verbose "Backup of '$Database' completed successfully."
    }
    catch {
        Write-Verbose "Backup step finished for '$Database'."
    }
}

[CmdletBinding()], a mandatory typed parameter, Write-Verbose instead of Write-Host: every individual habit here is exactly what a PowerShell style guide asks for, and a reviewer scanning for those habits finds them all present. What's missing is any actual handling in the catch block. $_ is never read, nothing is logged to the caller, nothing is re-thrown. The failure is swallowed just as completely as the ugly version, but it's swallowed inside a function that looks like it was written by someone who knows what they're doing, which is exactly why it will pass a quick review.

C#: iterating a result set

Obviously wrong — no using block, no braces discipline, easy to distrust on sight
public void ProcessRows(DataTable dt){
SqlConnection c=new SqlConnection(connStr);
c.Open();
for(int i=0;i<=dt.Rows.Count;i++){
var row=dt.Rows[i];
DoWork(row);
}
}
Pretty, generated, and still off by one
public void ProcessRows(DataTable table)
{
    using var connection = new SqlConnection(_connectionString);
    connection.Open();

    for (var i = 0; i <= table.Rows.Count; i++)
    {
        var row = table.Rows[i];
        ProcessRow(connection, row);
    }
}

The using declaration is a real improvement, the connection really will be disposed correctly, and that's precisely the problem: it's a genuine sign of care that a reviewer notices and, having noticed it, relaxes. The loop bound is still <= instead of <, and table.Rows[table.Rows.Count] will throw IndexOutOfRangeException on every non-empty table. One correct, visible habit bought cover for one incorrect, easy-to-miss one, sitting three lines apart in the same tidy method.

Notice what stayed constant across all three pairs: the bug. What changed was the amount of surrounding evidence that the author knew what they were doing. That evidence used to be reasonably trustworthy. Now it's manufactured by default, whether or not it's true.

Why This Is Worse Than It Sounds

It would be tempting to read this as "AI code needs the same review as human code," and stop there. It actually needs more scrutiny in one specific way: humans write ugly-and-wrong code often enough that reviewers built a muscle for it, but they never had to build a muscle for pretty-and-wrong, because it used to be rare. That muscle doesn't transfer automatically. A reviewer who has spent years pattern-matching on formatting quality as a proxy for correctness is, ironically, the person most likely to get lulled by a well-formatted sp_executesql call or a disciplined using block. The instinct that used to protect them is now the thing being exploited, not maliciously, just as a side effect of what the model was optimized to produce: plausible, well-formatted code, not necessarily correct code.

What Actually Still Works

None of this means review is hopeless, it means the cheap first-pass heuristic is gone and the remaining ones have to carry more weight than before.

Sloppy code was never the actual problem, it was a symptom that happened to be visible. AI has made the symptom disappear without touching the underlying rate of mistakes. The formatting got better. The discipline required to find what's actually wrong underneath it just got harder, at exactly the moment more code is shipping with less human attention on any single line of it.