> Где должна происходить обработка ошибок: в сервисе или в 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, для другого - несущественно.
Правильная модель - трёхслойная:
- Сервис - бросает/возвращает доменную ошибку.
- ViewModel/Presenter (если есть) - решает, какое состояние UI установить (error, empty, partial).
- View - рендерит состояние.
В iOS это особенно важно из-за Combine и async/await: ошибки должны быть Error-типами, а не Result с сырыми NSError.
На практике
Для senior-позиции важно показать, что вы понимаете границы слоёв и умеете проектировать API сервиса так, чтобы UI оставался тонким. Практические паттерны:
- сервис возвращает
Result<Model, AppError>илиasync throws -> ModelсAppError; AppError- enum с associated values, conforming toLocalizedError;- UI работает только с
AppError, никогда сURLErrorилиDecodingError; - если ошибка не критична для конкретного экрана, сервис может вернуть
Result<Model?, AppError>- но это скорее исключение; - для сетевых ошибок полезно в сервисе добавлять
isRetryableфлаг илиretryAfter- UI не должен знать про HTTP-коды.
Также важно: не путать "обработку" и "презентацию". Обработка - это логика (что делать с ошибкой: retry, fallback, ignore). Презентация - это только текст и иконка. Логика обработки может быть и в сервисе (например, авто-retry для 401), но решение "показать alert" - всегда UI.
Пример кода
SWIFT// Сервисный слойenum NetworkError: Error {case timeoutcase serverError(code: Int)case noConnection}enum AuthError: Error {case invalidCredentialscase userBlockedcase 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 = falsefunc login() {isLoading = trueTask {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-специфики:
Errorprotocol,LocalizedError,Result, async/await; - способность аргументировать trade-off: где дублирование неизбежно, а где - признак плохого дизайна;
- практический опыт: как вы решали конфликт "сервис хочет retry, UI хочет показать ошибку".
Типичные ошибки
- "Вся обработка в UI" - кандидат не понимает, почему сервис должен скрывать детали сети.
- "Вся обработка в сервисе" - кандидат игнорирует пользовательский опыт и локализацию.
- Возврат сырых
NSErrorилиURLErrorиз сервиса - признак слабого дизайна. - Использование
Error.localizedDescriptionбез кастомных сообщений - UI получает технический текст. - Отсутствие доменного enum - кандидат не умеет моделировать ошибки.
- Смешение retry-логики и презентации: например, UI сам решает, когда повторить запрос, вместо сервиса.
> Похожие задачи по mobile
Пробовали ли верстать под iPad и iPhone одновременно
Нужно ли проверять продукт или ошибку в методе loadData, если результат уже обработан
Какой метод UITableView используется для dequeue ячейки
Почему в Swift используется camelCase, а на бэке snake_case
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью