> Где должна происходить обработка ошибок: в сервисе или в UI (iOS, Swift)

Уровень: senior · Роль: mobile · Категория: Технические вопросы

Компании: Ozon

Стек: iOS, Swift

> Пример ответа

Короткий ответ

Обработка ошибок должна быть распределённой: сервис отвечает за обнаружение, классификацию и трансформацию ошибок в доменные типы, а UI - за их презентацию и user-friendly сообщения. Сервис не должен знать об UIKit, а UI не должен разбирать сырые NSError или сетевые коды. Граница проходит по доменному слою: сервис возвращает типизированную ошибку, UI решает, что с ней делать.

Подробное объяснение

Ключевой принцип - разделение ответственности. Сервис (сетевой слой, repository, use case) отвечает за:

  • перехват низкоуровневых ошибок (URLError, DecodingError, NSError);
  • маппинг их в доменные типы (например, AuthError.invalidCredentials, NetworkError.timeout);
  • логирование и аналитику ошибок;
  • retry-логику, если она не зависит от UI.

UI отвечает за:

  • отображение сообщения пользователю (alert, toast, inline error);
  • выбор тона и формулировки (техническая детализация vs человеческий язык);
  • действия пользователя после ошибки (retry button, fallback screen);
  • локальное состояние ошибки (например, disabled button при loading).

Почему нельзя всю обработку в UI: UI становится хрупким, дублируется логика маппинга, теряется контекст (например, сервис знает, что ошибка retryable, а UI - нет). Почему нельзя всю в сервисе: сервис не знает, как пользователь должен воспринять ошибку - для одного экрана это fatal, для другого - несущественно.

Правильная модель - трёхслойная:

  1. Сервис - бросает/возвращает доменную ошибку.
  2. ViewModel/Presenter (если есть) - решает, какое состояние UI установить (error, empty, partial).
  3. View - рендерит состояние.

В iOS это особенно важно из-за Combine и async/await: ошибки должны быть Error-типами, а не Result с сырыми NSError.

На практике

Для senior-позиции важно показать, что вы понимаете границы слоёв и умеете проектировать API сервиса так, чтобы UI оставался тонким. Практические паттерны:

  • сервис возвращает Result<Model, AppError> или async throws -> Model с AppError;
  • AppError - enum с associated values, conforming to LocalizedError;
  • UI работает только с AppError, никогда с URLError или DecodingError;
  • если ошибка не критична для конкретного экрана, сервис может вернуть Result<Model?, AppError> - но это скорее исключение;
  • для сетевых ошибок полезно в сервисе добавлять isRetryable флаг или retryAfter - UI не должен знать про HTTP-коды.

Также важно: не путать "обработку" и "презентацию". Обработка - это логика (что делать с ошибкой: retry, fallback, ignore). Презентация - это только текст и иконка. Логика обработки может быть и в сервисе (например, авто-retry для 401), но решение "показать alert" - всегда UI.

Пример кода

SWIFT
// Сервисный слой
enum NetworkError: Error {
case timeout
case serverError(code: Int)
case noConnection
}
enum AuthError: Error {
case invalidCredentials
case userBlocked
case network(NetworkError)
}
protocol AuthServiceProtocol {
func login(email: String, password: String) async throws -> User
}
final class AuthService: AuthServiceProtocol {
func login(email: String, password: String) async throws -> User {
do {
let data = try await apiClient.request(...)
return try decoder.decode(User.self, from: data)
} catch let error as NetworkError {
throw AuthError.network(error)
} catch let error as DecodingError {
throw AuthError.invalidCredentials // или отдельный case
}
}
}
// UI слой
final class LoginViewModel: ObservableObject {
@Published var errorMessage: String?
@Published var isLoading = false
func login() {
isLoading = true
Task {
do {
let user = try await authService.login(email: email, password: password)
// success state
} catch let error as AuthError {
errorMessage = error.localizedDescription
} catch {
errorMessage = "Неизвестная ошибка"
}
isLoading = false
}
}
}

Как отвечать на собеседовании

Начните с чёткого тезиса: "Обработка распределена, но граница - по доменному типу ошибки". Затем покажите слои: сервис маппит, UI презентует. Приведите пример из практики - например, как вы отделяли retry-логику от alert-сообщений. Подчеркните, что сервис не должен импортировать UIKit, а UI не должен знать про URLSession. Если спросят про конкретные кейсы (401, offline, partial data) - объясните, где принимается решение: 401 - сервис (refresh token), offline - сервис (проверка reachability), но сообщение - UI.

Что проверяет интервьюер

  • понимание слоёв архитектуры и границ ответственности;
  • умение проектировать типизированные ошибки;
  • знание Swift-специфики: Error protocol, LocalizedError, Result, async/await;
  • способность аргументировать trade-off: где дублирование неизбежно, а где - признак плохого дизайна;
  • практический опыт: как вы решали конфликт "сервис хочет retry, UI хочет показать ошибку".

Типичные ошибки

  • "Вся обработка в UI" - кандидат не понимает, почему сервис должен скрывать детали сети.
  • "Вся обработка в сервисе" - кандидат игнорирует пользовательский опыт и локализацию.
  • Возврат сырых NSError или URLError из сервиса - признак слабого дизайна.
  • Использование Error.localizedDescription без кастомных сообщений - UI получает технический текст.
  • Отсутствие доменного enum - кандидат не умеет моделировать ошибки.
  • Смешение retry-логики и презентации: например, UI сам решает, когда повторить запрос, вместо сервиса.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью