Многие баги, на первый взгляд, зарыты в коде. Но что, если код — это просто зеркало нашего мышления, а баг — результат когнитивной ошибки, которую мы даже не осознали? Эта статья — ретроспектива инженерных провалов, где причина — не баг в логике, а баг в голове. Разбираемся, почему мы думаем криво, как это ломает код, и можно ли «дебажить» собственное мышление.

Введение: баг, которого не было
Были ли у вас баги, которые исчезли, когда вы начали их кому-то объяснять? Или, наоборот, баги, которые вы не видели часами, пока не посмотрели на них с совершенно другой стороны? Я хочу поговорить не о коде. И даже не о компиляторах, алгоритмах или архитектуре. А о той странной штуке, в которой всё это происходит — о мозге разработчика.
Большинство багов начинается не с if, а с «я точно всё понял». Не с null, а с «не может такого быть». И не с segfault, а с «я же так делал сто раз». То есть — с багов мышления.
1. Раздел первый: баг как симптом мышления
Код — это манифестация наших мыслей. Баг — это симптом ошибочной логики, прошедшей через компилятор. Компиляторы не догадываются, что вы имели в виду. Они работают строго по инструкции. А инструкция — это и есть следствие вашего мышления.
Пример
# Я уверен, что переменная всегда инициализирована до вызова
def calculate_discount(price):
    if has_discount:
        return price * 0.9
    return priceОшибка очевидна? Только если вы привыкли думать о переменных как о сущностях, которым надо дать значение до использования. А если вы переучились с языка, где has_discount глобальная или имеет дефолт — вы можете и не заметить.
Это не ошибка кода. Это ошибка в предположениях.
3. Мышление — не поток, а сеть
Мы любим представлять мышление как поток: получил задачу → подумал → написал код → протестировал. Но реальность — это сеть взаимосвязей, ожиданий, эвристик, привычек и когнитивных искажений.
Мы редко задумываемся, что значат слова «всё работает». Работает как? В каких условиях? А почему ты считаешь, что оно работает? Потому что прошёл тест? А тест написан в том же багнутом контексте мышления.
4. Когнитивные искажения: чем багат наш мозг
Некоторые классические баги в коде — это просто отражения стандартных когнитивных искажений:
- Иллюзия прозрачности — «ясно же, как работает этот метод»; 
- Предвзятость подтверждения — мы читаем код, чтобы доказать, что он работает, а не искать, где он не работает; 
- Эффект Даннинга-Крюгера — чем меньше мы понимаем, тем увереннее пишем код; 
- Слепое пятно предвзятости — «ну я-то объективен». 
Каждое из этих искажений находит свой способ сломать ваш прод.
5. Язык как ловушка мышления
Языки программирования задают каркас мышления. Например:
let isAdmin = false;
if (isAdmin = true) {
    // always runs
}JavaScript позволяет такую конструкцию, потому что = возвращает значение. Вы думаете, что проверяете, а на самом деле присваиваете. И это баг не в языке. Это баг в том, как мозг ожидает работу конструкции. Язык формирует эти ожидания. А потом ломает их.
6. Архитектура «невидимого бага»
Самые сложные баги — это не сломанный for и не неправильный index. Это баги, встроенные в архитектуру, в саму модель системы. Они сидят на фундаментальном предположении. На чём-то вроде:
- «Событие не может произойти дважды подряд»; 
- «Состояние X всегда следует за состоянием Y»; 
- «Пользователь не нажмёт эту кнопку три раза за секунду». 
Ни одна из этих предпосылок не проверена. Все они — просто убеждения разработчика. То есть баги мышления.
7. Диалог как способ дебага головы
Когда объясняешь кому-то свой баг, часто ловишь себя: «блин, а что если тут…» — и ты находишь ошибку. Это называется «эффект резиновой уточки». Но он работает не из-за уточки, а потому, что ты пересобираешь свою модель мышления, когда вербализуешь её.
8. Как баги проходят ревью и тесты
Классика: «пропустили баг на ревью». Почему?
- Читали глазами, а не умом; 
- Имеют ту же ментальную модель, что и автор кода; 
- Проверяют «соответствие стандартам», а не «подразумеваемую логику». 
Ревью, в сущности, не проверка кода. Это попытка сравнить мышление автора и ревьюера. Если оно одинаково кривое — баг пройдет.
9. Можно ли тестировать мышление?
Да. Это называется «деструктивное мышление». В программировании мы тестируем код. А вот в инженерии высокого уровня тестируют гипотезы и предпосылки.
Пример:
# Python: проверим предположение о порядке вызовов
calls = []
def a():
    calls.append('a')
def b():
    calls.append('b')
def c():
    a()
    b()
    return calls
print(c())  # ['a', 'b']Теперь предположим, что a() вызывает b() внутри. Если ты об этом не знаешь, поведение сломается. Проблема не в коде. Проблема в том, что ты думал иначе.
10. Как не попасть в мышленческий баг
Никак. Мы все люди. Но можно:
- Обсуждать логику с людьми, которые мыслят по-другому; 
- Вводить практики тестирования предпосылок; 
- Добавлять в код комментарии не только «что делает», но и «почему так»; 
- Развивать паранойю: «а что если всё не так, как я думаю?»; 
- Писать код, будто тебе будет его объяснять враг на суде. 
11. (Немного личного) Почему я пишу об этом
Потому что у меня был баг. Странный, непонятный, абсолютно неуловимый. И когда я его нашёл — я понял, что всё это время думал не так. Не кодил, не проектировал, а просто думал криво. Я уверен, у каждого был такой баг.
И может быть, если мы научимся замечать свои баги мышления, багов в коде станет чуть меньше.
Если ты дочитал до этого момента — возможно, у нас с тобой была похожая архитектура мышления. Или баг в ней. В любом случае, спасибо, что заглянул в глубину кода, где баги начинают свой путь — в нашей голове))
Комментарии (10)
 - OlegZH28.07.2025 14:06- Теперь предположим, что - a()вызывает- b()внутри.- Это как? И что будет-то? Это надо - b()определить до- a(). Надо как-то яснее приводить пример. - Hardcoin28.07.2025 14:06- Вы проверяли это свое предположение, что нужно b определить до а? Автор ведь об этом и пишет, о багах мышления, а вы сходу иллюстрируете.  - OlegZH28.07.2025 14:06- Ужасно рад что-то проиллюстрировать. ;-) К сожалению, я не очень понятливый. Я не понял примера. Да, я (теперь!) проверил возможность вызова b() внутри a() . Но я не понял, что сломалось и где? Если написать - def a(): b() calls.append('a')- то получится - ['b', 'a', 'b']- Если написать - ef a(): calls.append('a') b()- то получится - ['a', 'b', 'b']- Всё ожидаемо. А что же сломалось? 
 
 
 - hex_coder28.07.2025 14:06- После пункта 1 идёт пункт 3. Это специально так задумано, чтобы узнать, сколько прочитавших это незаметит или проигнорирует? - А вообще, самые ужасные ошибки - синтаксические, которые по совпадению пропускает компилятор. Ты на самом деле не думал так, это оно так напечаталось, а в этот момент думал о следующих строчках/идеях. Та же штука про равно в условии, и дело уже не только в языке, а в языке + подсознании. То есть если бы там стояло что-то другое, то мозг бы заметил, а так он это пропускает. 
 - Racheengel28.07.2025 14:06- Баги после внедрения многопоточности особо жестокие... ломается даже тем, где, казалось бы, на века. 
 - sunUnderShadow28.07.2025 14:06- Ставьте топ, если тоже забываете "=" в сравнениях - if (isAdmin = true ){}- Без негатива, просто забавно  - Nikita2200728.07.2025 14:06- Один раз я таки попался на такую проблему. После этого научился читать варнинги. - Условия Йоды не просто так придумыли.  - OlegZH28.07.2025 14:06- Думаю, проблемы с присваиванием не было бы, если не было неявного приведения типов. Или, если бы, оно было бы более контролируемым. Тут вопрос в том, почему, вообще, выражение "a=b" может быть проинтерпретировано как логическое. 
 
 
 
           
 
ahdenchik
Половина описанных багов стали возможны из-за плохого дизайна использованных языков