Никак. Так или иначе, в сложном коде баги возникают по той или иной причине, даже если код тщательно тестируется. Учесть все возможные наборы входных данных для всех функций очень сложно, всегда есть риск что какой-то случай не протестирован.
Правильный вопрос "как писать код с минимумом багов".
Ответ: следовать правилам, что давно уже разработаны для программистов, например SOLID (и не только), и писать unit-тесты.
Старайтесь любой код сводить к набору маленьких функций, которые уже вызываются функциями уровнем выше, и ещё выше.
Начиная с самых небольших, простых функций, пишите unit-тесты на них с хотя бы каким-то минимальным набором данных. Например, если в функции есть математические расчёты с делением - не забывайте проверить функцию тестом с входными данными, приводящими к делению на ноль (да, это совсем примитивный пример, но зато наглядный), убедитесь что функция вернёт корректную и понятную ошибку.
Любую сколько-то сложную систему просто невозможно протестировать на 100% "извне" (например, через API), потому именно unit-тесты на каждую, даже самую незначительную, казалось бы, функцию, просто необходимы.
Также набор тестов защищает от регрессии кода. Зачем это нужно? Потому что, если вы тестируете функции только вручную, то через год или два (а может и раньше) туда может кто-то полезть функцию менять, и это изменение приведёт к неработоспособности ранее работоспособного кода, т.к. функция вела себя некоторым определённым образом, это поведение было протестировано, всё стабильно работало, а затем по необходимости функцию модифицировали, но допустили ошибку, которая привела к проблемам в ряде функций, что используют данную функцию.