Почему ты знаешь Coroutines, но валишься на собеседовании: разбор 6 типичных ошибок
Kotlin Coroutines
15 мин чтения 3 августа 2026

Почему ты знаешь Coroutines, но валишься на собеседовании: разбор типичных ошибок

Ты читал документацию и писал viewModelScope.launch десятки раз. Но на собеседовании уверенность испаряется. Разбираем, где именно ломаются ответы у кандидатов с 1–2 годами опыта.

Ты читал документацию. Смотрел видео. Писал viewModelScope.launch десятки раз. Можешь объяснить, что такое suspend-функция. Но на собеседовании, после третьего уточняющего вопроса про корутины, уверенность испаряется.

Проблема не в том, что ты не знаешь Coroutines. Проблема в том, что ты знаешь их на уровне API, а собеседование проверяет на уровне понимания. Разница — пропасть. И именно в неё падают 8 из 10 кандидатов с 1–2 годами опыта.

Ниже — разбор типичных вопросов, на которых сыплются. Не теория из документации, а то, что реально спрашивают и где именно ломаются ответы.

Ошибка №1

«Dispatchers.IO — для тяжёлых вычислений, Dispatchers.Default — для сети»

«Dispatchers.IO нужен для ввода-вывода: сеть, файлы, база данных. Dispatchers.Default для CPU-тяжёлых задач: парсинг JSON, сортировка.»

Это заученная фраза из первого попавшегося туториала. Она формально верна, но интервьюер сразу копнёт глубже:

  • А сколько потоков в Dispatchers.IO и в Dispatchers.Default?
  • Что произойдёт, если я запущу 200 сетевых запросов на Dispatchers.Default?
  • Можно ли переключиться внутри корутины с одного диспетчера на другой? Как?
  • Dispatchers.Default ограничен количеством ядер CPU (Runtime.getRuntime().availableProcessors()). Он для задач, которые грузят процессор.
  • Dispatchers.IO имеет пул до 64 потоков (или больше) и оптимизирован под блокирующие операции. Потоки могут переиспользоваться между IO и Default.
  • Сетевые запросы через OkHttp/Retrofit уже асинхронны внутри. Запускать их на Dispatchers.IO часто избыточно.
  • Переключение — через withContext(Dispatchers.IO) { ... }. Это не создаёт новую корутину, а перевешивает текущую на другой диспетчер.

«Dispatchers — это не про тип задачи, а про характер нагрузки. IO — для операций, где поток большую часть времени ждёт. Default — для операций, где поток реально считает. Если я запускаю Retrofit-запрос, мне не нужен IO, потому что OkHttp сам управляет своими потоками. А вот если я читаю большой файл побайтово — тогда да, withContext(Dispatchers.IO)

Ошибка №2

«structured concurrency — это когда корутины вложены друг в друга»

«Structured concurrency означает, что дочерние корутины привязаны к родителю и отменяются вместе с ним.»

Это определение из вики, а не понимание. Интервьюер спросит:

  • Как распространяется отмена и исключения между родителем и детьми?
  • Что будет, если дочерняя корутина выбросит исключение? Умрёт ли родитель?
  • А если я не хочу, чтобы умирал?
  • Корутина, запущенная в CoroutineScope, привязана к его Job. Отмена scope → отмена всех детей.
  • GlobalScope.launch — нарушение structured concurrency. В Android это почти всегда баг.
  • Если дочерняя корутина падает с исключением, по умолчанию родитель тоже отменяется. Это fail-fast behaviour.
  • Чтобы изолировать падение — SupervisorJob или supervisorScope.

«Structured concurrency — это гарантия, что у каждой корутины есть родитель и она не переживёт свой scope. В Android это критично: viewModelScope привязан к ViewModel, lifecycleScope — к Lifecycle. Если ViewModel очищается, все корутины отменяются, и я не получаю утечку. Если нужно, чтобы падение одной задачи не убивало остальные, я использую SupervisorJob — например, при загрузке независимых секций экрана параллельно.»

Хочешь разобрать structured concurrency на живом примере?

На менторских сессиях мы пишем код, ломаем его и смотрим, что произойдёт. Не теорию, а реальные сценарии из прода.

Записаться на бесплатный разбор →
Ошибка №3

«Утечка памяти через корутины? Ну, я же в viewModelScope запускаю»

«Если использовать viewModelScope или lifecycleScope, утечек не будет, потому что scope сам отменяется.»

  • Что если внутри корутины ты держишь ссылку на Activity?
  • А если используешь GlobalScope или создаёшь свой CoroutineScope и забываешь отменить?
  • Что будет, если suspend-функция бесконечно ждёт в delay() или в блокирующем вызове?
  • viewModelScope защищает от отмены корутины, но не от утечки объектов внутри неё. Если внутри launch ты захватил ссылку на Activity или Context — утечка.
  • GlobalScope.launch — корутина живёт, пока не завершится сама. Если внутри бесконечный цикл — это утечка.
  • Блокирующие вызовы (Thread.sleep, синхронный IO) на Main не отменяются через Job.cancel().

«viewModelScope закрывает 90% случаев, но не все. Если я внутри корутины держу ссылку на View или Context, и корутина переживает их lifecycle — утечка. Поэтому я передаю applicationContext или использую WeakReference. И я помню, что cancel() корутины не прерывает блокирующий код — только suspend-точки. Если нужно прервать тяжёлую операцию, я проверяю isActive или использую withTimeout.»

Ошибка №4

«SupervisorJob — это чтобы корутины не отменялись»

«SupervisorJob нужен, чтобы при падении одной корутины не отменялись остальные.»

  • В чём разница между supervisorScope и CoroutineScope(SupervisorJob())?
  • Если использую SupervisorJob, исключения вообще куда-то деваются?
  • Что будет, если не поставить CoroutineExceptionHandler?
  • SupervisorJob меняет направление распространения исключений: ребёнок больше не отменяет родителя и братьев. Но исключение не исчезает — оно поднимается до CoroutineExceptionHandler или крашит приложение.
  • supervisorScope { } — структурный аналог. Создаёт scope с SupervisorJob внутри текущей корутины.
  • Без CoroutineExceptionHandler в scope с SupervisorJob необработанное исключение приведёт к крашу.

«SupervisorJob изолирует детей, но не проглатывает исключения. Мне всё равно нужен CoroutineExceptionHandler, иначе приложение упадёт. Я использую supervisorScope, когда загружаю несколько независимых блоков UI: если один упадёт, остальные доедут. Логирую ошибку через handler и показываю заменитель вместо краша.»

Узнал себя в этих ошибках?

Это значит, что пробелы в Coroutines мешают тебе звучать как middle. На диагностике за 120 минут мы разберём твой уровень и составим план закрытия именно этих пробелов.

Узнать про диагностику →
Ошибка №5

«withContext и async — это одно и то же, просто синтаксис другой»

«async возвращает Deferred, а withContext — результат напрямую. Но по сути оба переключают контекст.»

  • Что будет, если вызвать async и не вызвать await()?
  • withContext внутри async — это норма?
  • Если нужно запустить 10 запросов параллельно и дождаться всех?
  • async запускает новую корутину и возвращает Deferred. Результат не появится, пока не вызовешь await(). Если не вызвать — корутина отработает впустую, а исключение потеряется.
  • withContext не создаёт новую корутину. Это suspend-функция, которая переключает диспетчер внутри текущей корутины.
  • Для параллельных запросов — несколько async + awaitAll().

«async — это запуск параллельной задачи. withContext — переключение внутри текущей задачи. Если нужно параллельно загрузить профиль и уведомления — делаю два async и awaitAll. Если переключиться на IO для чтения кэша — withContext. И я помню: async без await — молча проглоченное исключение.»

Ошибка №6

«Отмена корутины — это просто вызвать job.cancel()»

«Вызываю job.cancel(), и корутина останавливается.»

  • Если внутри корутины цикл for (i in 1..1_000_000) без suspend-точек?
  • Что такое cooperative cancellation?
  • cancel() vs cancelAndJoin()?
  • Отмена в Coroutines — кооперативная. Корутина проверяет флаг отмены только в suspend-точках: delay(), yield(), withContext(), await().
  • Если плотный CPU-цикл без suspend-вызовов, cancel() не прервёт его. Нужно вручную проверять isActive или ensureActive().
  • job.cancelAndJoin() — ждёт, пока корутина реально завершится.
  • withTimeout и withTimeoutOrNull — тоже механизм отмены через CancellationException.

«Отмена — это не kill, а просьба. Корутина увидит её только в следующей suspend-точке. Если тяжёлый цикл — ставлю yield() или проверяю isActive на каждой итерации. Если нужно дождаться реальной остановки перед следующим шагом — cancelAndJoin. И помню, что CancellationException — это не ошибка, а нормальное завершение.»

Что объединяет все ошибки

Одна и та же причина: знание API без понимания модели.

Курсы учат синтаксису: «напиши launch», «вызови withContext», «поставь Dispatchers.IO». Но не учат думать в терминах coroutine, не учат тому, как распространяются исключения, кто принадлежит к lifecycle.

Проверяют не «знаешь ли, что такое SupervisorJob», а «понимаешь ли, зачем он и что сломается, если не поставить».

Что делать прямо сейчас

  • Перестань зубрить определения. Вместо «structured concurrency — это...» объясни вслух, что произойдёт в конкретном сценарии.
  • Пиши мини-примеры и ломай их. Запусти корутину, отмени её, брось исключение, посмотри, что выживет.
  • Проговаривай ответы вслух. Формат «я бы сделал X, потому что Y, а если не так — Z».
  • Разбирай реальные вопросы с собеседований. Не абстрактные «расскажи про корутины», а конкретные: «что вернёт этот код», «где утечка», «как это отменить».

Частые вопросы по Coroutines на собеседованиях

Чем withContext отличается от async?

async запускает новую корутину и возвращает Deferred. Результат не появится, пока не вызовешь await(). withContext не создаёт новую корутину — это suspend-функция, которая переключает диспетчер внутри текущей корутины. Для параллельных запросов — несколько async + awaitAll(). Для переключения контекста — withContext.

Что такое structured concurrency?

Гарантия, что у каждой корутины есть родитель и она не переживёт свой scope. В Android это критично: viewModelScope привязан к ViewModel, lifecycleScope — к Lifecycle. Если ViewModel очищается, все корутины отменяются. Если нужно, чтобы падение одной задачи не убивало остальные, используй SupervisorJob.

Сколько потоков в Dispatchers.IO и Dispatchers.Default?

Dispatchers.Default ограничен количеством ядер CPU. Dispatchers.IO имеет пул до 64 потоков и оптимизирован под блокирующие операции. Сетевые запросы через OkHttp/Retrofit уже асинхронны внутри — запускать их на Dispatchers.IO часто избыточно.

Что будет если не вызвать await() после async?

Корутина отработает впустую, а исключение потеряется. Это молча проглоченная ошибка. Всегда вызывай await() или используй awaitAll() для нескольких параллельных задач.

Как отменить корутину с плотным циклом без suspend-вызовов?

Отмена в Coroutines — кооперативная. Корутина проверяет флаг отмены только в suspend-точках. Если плотный CPU-цикл без suspend-вызовов, cancel() не прервёт его. Нужно вручную проверять isActive или ensureActive() на каждой итерации.

Р

Рустем Бикбулатов

Senior Android Developer · Ментор

Провожу технические собеседования и помогаю Android-разработчикам расти до middle и senior. Более 100 учеников и разборов. Эти ошибки — то, что я разбираю с менти на первых сессиях, чтобы собеседование перестало быть лотереей.

Про менторство Telegram-канал

Читайте также

Другие материалы для подготовки к собеседованию

Резюме vs «робот»: как пройти фильтры hh.ru

Разбор алгоритмов hh.ru: как резюме проходит автоматические фильтры, основные ошибки кандидатов и практичные рекомендации для Android-разработчиков.

Читать статью

5 задач по корутинам из реальных проектов

Практические задачи с контекстом из прода: cancellation, race condition, exception handling, Flow vs StateFlow, dispatchers.

Получить задачи бесплатно