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

Введение: баг, которого не было

Были ли у вас баги, которые исчезли, когда вы начали их кому-то объяснять? Или, наоборот, баги, которые вы не видели часами, пока не посмотрели на них с совершенно другой стороны? Я хочу поговорить не о коде. И даже не о компиляторах, алгоритмах или архитектуре. А о той странной штуке, в которой всё это происходит — о мозге разработчика.

Большинство багов начинается не с 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)


  1. ahdenchik
    28.07.2025 14:06

    Половина описанных багов стали возможны из-за плохого дизайна использованных языков


  1. OlegZH
    28.07.2025 14:06

    Теперь предположим, что a() вызывает b() внутри.

    Это как? И что будет-то? Это надо b()определить до a(). Надо как-то яснее приводить пример.


    1. Hardcoin
      28.07.2025 14:06

      Вы проверяли это свое предположение, что нужно b определить до а? Автор ведь об этом и пишет, о багах мышления, а вы сходу иллюстрируете.


      1. OlegZH
        28.07.2025 14:06

        Ужасно рад что-то проиллюстрировать. ;-) К сожалению, я не очень понятливый. Я не понял примера. Да, я (теперь!) проверил возможность вызова b() внутри a() . Но я не понял, что сломалось и где? Если написать

        def a():
            b()
            calls.append('a')

        то получится

        ['b', 'a', 'b']

        Если написать

        ef a():
            calls.append('a')
            b()

        то получится

        ['a', 'b', 'b']

        Всё ожидаемо. А что же сломалось?


  1. hex_coder
    28.07.2025 14:06

    После пункта 1 идёт пункт 3. Это специально так задумано, чтобы узнать, сколько прочитавших это незаметит или проигнорирует?

    А вообще, самые ужасные ошибки - синтаксические, которые по совпадению пропускает компилятор. Ты на самом деле не думал так, это оно так напечаталось, а в этот момент думал о следующих строчках/идеях. Та же штука про равно в условии, и дело уже не только в языке, а в языке + подсознании. То есть если бы там стояло что-то другое, то мозг бы заметил, а так он это пропускает.


  1. Racheengel
    28.07.2025 14:06

    Баги после внедрения многопоточности особо жестокие... ломается даже тем, где, казалось бы, на века.


  1. sunUnderShadow
    28.07.2025 14:06

    Ставьте топ, если тоже забываете "=" в сравнениях

    if (isAdmin = true ){}

    Без негатива, просто забавно


    1. Nikita22007
      28.07.2025 14:06

      Один раз я таки попался на такую проблему. После этого научился читать варнинги.

      Условия Йоды не просто так придумыли.


      1. OlegZH
        28.07.2025 14:06

        Думаю, проблемы с присваиванием не было бы, если не было неявного приведения типов. Или, если бы, оно было бы более контролируемым. Тут вопрос в том, почему, вообще, выражение "a=b" может быть проинтерпретировано как логическое.