Асинхронность — фундаментальное свойство JavaScript, но обработка ошибок в этом контексте остается источником утечек памяти, лакун в логике и непредсказуемого поведения. Рассмотрим стратегии, превосходящие базовые try/catch
, которые защитят ваше приложение от тихих сбоев.
Ограничения стандартных подходов
Типичный async/await
с try/catch
страдает избыточностью и скрывает контекст:
async function fetchUserData(userId) {
try {
const user = await fetch(`/users/${userId}`);
const posts = await fetch(`/posts?user=${userId}`);
return { ...user, posts };
} catch (error) {
console.error("Ошибка загрузки данных");
throw error; // Теряется информация о источникe ошибки
}
}
Здесь:
- Неясно, какой запрос провалился.
- Нет стека вызовов для асинхронных операций.
- Повторяющийся код при масштабировании.
Стратегии промышленной обработки
Преобразование ошибок
Обогащение ошибки метаданными:
class NetworkError extends Error {
constructor(url, statusCode, context) {
super(`Сбой при обращении к ${url}`);
this.statusCode = statusCode;
this.context = context;
}
}
async function safeFetch(url, options) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new NetworkError(url, response.status, 'HTTP_ERROR');
}
return await response.json();
} catch (error) {
error.url = url; // Аттач ссылки даже для сетевых сбоев
throw error;
}
}
Теперь:
- Ошибки типизированы.
- Стек включает кастомный контекст.
- Логирование получает структурированные данные.
Агрегация ошибок при параллелизме
Promise.all
терпит неудачу при первом сбое. Promise.allSettled
тайно игнорирует ошибки. Решение — гибридный подход:
async function fetchAll(resources) {
const results = await Promise.allSettled(
resources.map(resource => safeFetch(resource.url, resource.options))
);
const errors = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (errors.length > 0) {
throw new AggregateError(errors, 'Multiple fetching errors');
}
return results.map(r => r.value);
}
Паттерн Wrapper
Обертки улучшают читабельность, избегая вложенных try/catch
:
function asyncHandler(fn) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
addErrorMetadata(error, { function: fn.name });
throw error;
}
};
}
const safeUserData = asyncHandler(async userId => {
const user = await safeFetch(`/users/${userId}`);
const posts = await safeFetch(`/posts?user=${userId}`);
return { ...user, posts };
});
Интеграция с фреймворками
Express: Мидлвар для централизации обработки
app.use(async (err, req, res, next) => {
if (err instanceof NetworkError) {
res.status(502).json({
error: 'Upstream failure',
endpoint: err.url
});
} else {
// Логика для других ошибок
}
});
React: Элегантное управление состоянием ошибок
const DataComponent = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
async function load() {
try {
setData(await safeUserData(123));
} catch (err) {
setError(err.userFacingMessage || 'Service unavailable');
}
}
load();
}, []);
return error ? <ErrorBoundary message={error} /> : <DataView data={data} />;
};
Куда отправлять ошибки: полноценная экосистема логгирования
Консоль не подходит для продакшен-аналитики. Сбор ошибок обязателен:
- Backend: Отправка в Sentry/DataDog через мидлвар
- Frontend: Интеграция Error Boundaries с логгером
class ReportingBoundary extends React.Component {
componentDidCatch(error, info) {
loggingClient.captureException(error, {
componentStack: info.componentStack,
userId: Auth.currentUserId()
});
}
}
Ключевые принципы
- Статический анализ: Используйте TypeScript для защиты от распространенных ошибок.
- Деградация в приложениях типа PWA: Кеширование стратегий и показателей восстанавливает offline-использование.
- Трассировка: Коррелируйте ошибки фронтенда с соответствующими логами бэкенда через идентификаторы запросов.
Обработка ошибок является проектной практикой, а не постскриптумом. Итеративное включение этих стратегий приводит к приложениям, которые не просто уведомляют о проблеме, но и предоставляют возможности для быстрого устранения.