Рассмотрите обработку ошибок и исключений в асинхронном коде на примере JavaScript (Promise/async-await) и языка с исключениями уровня системы (например, Java): как структурировать обработку ошибок, чтобы не терять контекст, избегать "поглощения" ошибок и корректно восстанавливать состояние приложения?
Кратко и по делу — принципы и практики, примеры для JavaScript (Promise / async–await) и для Java (исключения уровня системы). Цель: не терять контекст, не «глотать» ошибки, корректно восстанавливать состояние. Общие принципы - Разделяй «операционные»/ожидаемые ошибки (сеть, IO) и «программистские» (NullPointer, Assertion). Как правило, только операционные ловить и пытаться восстановить. - Не глуши ошибку без явного решения: либо логируй и пробрасывай, либо обрабатываешь и восстанавливаешь состояние. - Добавляй контекст при пробрасывании (correlation id, параметры операции). Используй объектную цепочку причин (cause). - Очистка/восстановление состояния всегда в finally / эквиваленте (или try-with-resources): не оставляй ресурсы/замки/транзакции открытыми. - Логируй на границах системы (вход/выход), не логируй одно и то же много раз. - Для параллельных задач используйте механизмы, гарантирующие распространение ошибок (Promise.allSettled/CompletableFuture with explicit handling). JavaScript — практики и примеры - async/await: оберни критичные участки в try/catch; при доп. контексте пробрасывай с сохранением original cause (Node 16+ поддерживает Error.cause). - Не делай пустой .catch(() => {}) и не игнорируй rejectы промисов. - Для массовых параллельных задач: если нужно, используйте Promise.allSettled, иначе Promise.all (он упадёт при первой ошибке). - Для отмены операций — используйте AbortController и передавайте signal, не принудительно бросайте ошибку в фоне. Примеры: async/await с сохранением причины (Node v16+): ```js async function loadUser(userId) { try { const data = await fetch(`/api/users/${userId}`); return await data.json(); } catch (err) { // добавляем контекст, сохраняем оригинальную причину throw new Error(`loadUser failed for ${userId}`, { cause: err }); } finally { // очистка / освобождение ресурсов } } ``` Promise-цепочка (не глушим ошибку): ```js return fetch(url) .then(res => res.json()) .catch(err => { throw new Error(`fetch ${url} failed`, { cause: err }); }); ``` Параллель и обработка нескольких ошибок: ```js const results = await Promise.allSettled(tasks); for (const r of results) { if (r.status === 'rejected') { // собрать контекст, логировать или агрегировать ошибки } } ``` Советы по JS - Используй Error subclasses для типизации ошибок. - При retry: проверяй идемпотентность операции и включай backoff; пробрасывай / возвращай оригинальную ошибку при исчерпании попыток. - Настраивай global handlers: process.on('unhandledRejection', ...) и window.onunhandledrejection, чтобы фиксировать «потерянные» reject’ы. Java — практики и примеры - Используй проверяемые исключения (checked) для ожидаемых/восстанавливаемых ошибок, unchecked для ошибок программирования. - При перехвате оборачивай исключение в более информативное, сохраняя cause: new MyAppException("context", e). - Для ресурсов — try-with-resources: автоматически сохраняет suppressed exceptions при ошибках в закрытии. - Для параллельных задач с CompletableFuture: не теряй оригинальный cause (CompletionException оборачивает исходное исключение — распаковывай при необходимости). Примеры: try-with-resources + оборачивание: ```java try (InputStream in = openStream(path)) { // работа с ресурсом } catch (IOException e) { throw new DataLoadException("Failed to load " + path, e); } ``` Транзакция и откат: ```java try { tx.begin(); doWork(); tx.commit(); } catch (Exception e) { try { tx.rollback(); } catch (Exception rb) { e.addSuppressed(rb); } throw new ServiceException("doWork failed for id=" + id, e); } ``` CompletableFuture — не теряем причину: ```java CompletableFuture.supplyAsync(() -> doRemote()) .whenComplete((res, ex) -> { if (ex != null) { Throwable cause = (ex instanceof CompletionException) ? ex.getCause() : ex; log.error("remote failed", cause); // обработать/перебросить как нужно } }); ``` Советы по Java - Не catch (Throwable) или catch (Exception) без разбора; если всё-таки ловите, явно пробрасывайте или логируйте с контекстом. - Используйте addSuppressed для ошибок при очистке, либо полагайтесь на try-with-resources. - Централизованный слой трансляции ошибок (например, контроллер → сервис → dao): ловим на границе и мапим на понятные ответы/статусы. Как правильно восстанавливать состояние (общие рекомендации) - Всегда планируй компенсацию: для каждой операции, изменяющей состояние, имей шаги отката (rollback/compensating action). - В async-коде гарантируй, что cleanup выполняется в finally/try-with-resources/whenComplete. - Не делай cleanup, который может скрыть исходную ошибку: если cleanup бросает исключение, присоедини его как suppressed / cause, а не затирай первичную ошибку. - Для долгих/распределённых процессов используйте идемпотентные операции или храните статус операции (например, durable saga), чтобы можно было корректно повторять/компенсировать. Короткая сводка «что делать» при проектировании: - Пробрасывай ошибки вверх с контекстом, не глуши их. - Чистка ресурсов в finally / try-with-resources / whenComplete. - Используй chainable cause/ suppressed для сохранения полной истории. - Логируй на границах и добавляй correlation id. - Для параллельных/асинхронных задач — явная обработка ошибок (allSettled, whenComplete), unwrap CompletionException/Promise rejections. - Для retry/отката — проверяй идемпотентность и имей компенсации. Если нужно — могу дать конкретные шаблоны/классы ошибок для вашего проекта (JS или Java) и примеры интеграции с логированием и корреляцией.
Общие принципы
- Разделяй «операционные»/ожидаемые ошибки (сеть, IO) и «программистские» (NullPointer, Assertion). Как правило, только операционные ловить и пытаться восстановить.
- Не глуши ошибку без явного решения: либо логируй и пробрасывай, либо обрабатываешь и восстанавливаешь состояние.
- Добавляй контекст при пробрасывании (correlation id, параметры операции). Используй объектную цепочку причин (cause).
- Очистка/восстановление состояния всегда в finally / эквиваленте (или try-with-resources): не оставляй ресурсы/замки/транзакции открытыми.
- Логируй на границах системы (вход/выход), не логируй одно и то же много раз.
- Для параллельных задач используйте механизмы, гарантирующие распространение ошибок (Promise.allSettled/CompletableFuture with explicit handling).
JavaScript — практики и примеры
- async/await: оберни критичные участки в try/catch; при доп. контексте пробрасывай с сохранением original cause (Node 16+ поддерживает Error.cause).
- Не делай пустой .catch(() => {}) и не игнорируй rejectы промисов.
- Для массовых параллельных задач: если нужно, используйте Promise.allSettled, иначе Promise.all (он упадёт при первой ошибке).
- Для отмены операций — используйте AbortController и передавайте signal, не принудительно бросайте ошибку в фоне.
Примеры:
async/await с сохранением причины (Node v16+):
```js
async function loadUser(userId) {
try {
const data = await fetch(`/api/users/${userId}`);
return await data.json();
} catch (err) {
// добавляем контекст, сохраняем оригинальную причину
throw new Error(`loadUser failed for ${userId}`, { cause: err });
} finally {
// очистка / освобождение ресурсов
}
}
```
Promise-цепочка (не глушим ошибку):
```js
return fetch(url)
.then(res => res.json())
.catch(err => {
throw new Error(`fetch ${url} failed`, { cause: err });
});
```
Параллель и обработка нескольких ошибок:
```js
const results = await Promise.allSettled(tasks);
for (const r of results) {
if (r.status === 'rejected') {
// собрать контекст, логировать или агрегировать ошибки
}
}
```
Советы по JS
- Используй Error subclasses для типизации ошибок.
- При retry: проверяй идемпотентность операции и включай backoff; пробрасывай / возвращай оригинальную ошибку при исчерпании попыток.
- Настраивай global handlers: process.on('unhandledRejection', ...) и window.onunhandledrejection, чтобы фиксировать «потерянные» reject’ы.
Java — практики и примеры
- Используй проверяемые исключения (checked) для ожидаемых/восстанавливаемых ошибок, unchecked для ошибок программирования.
- При перехвате оборачивай исключение в более информативное, сохраняя cause: new MyAppException("context", e).
- Для ресурсов — try-with-resources: автоматически сохраняет suppressed exceptions при ошибках в закрытии.
- Для параллельных задач с CompletableFuture: не теряй оригинальный cause (CompletionException оборачивает исходное исключение — распаковывай при необходимости).
Примеры:
try-with-resources + оборачивание:
```java
try (InputStream in = openStream(path)) {
// работа с ресурсом
} catch (IOException e) {
throw new DataLoadException("Failed to load " + path, e);
}
```
Транзакция и откат:
```java
try {
tx.begin();
doWork();
tx.commit();
} catch (Exception e) {
try { tx.rollback(); } catch (Exception rb) { e.addSuppressed(rb); }
throw new ServiceException("doWork failed for id=" + id, e);
}
```
CompletableFuture — не теряем причину:
```java
CompletableFuture.supplyAsync(() -> doRemote())
.whenComplete((res, ex) -> {
if (ex != null) {
Throwable cause = (ex instanceof CompletionException) ? ex.getCause() : ex;
log.error("remote failed", cause);
// обработать/перебросить как нужно
}
});
```
Советы по Java
- Не catch (Throwable) или catch (Exception) без разбора; если всё-таки ловите, явно пробрасывайте или логируйте с контекстом.
- Используйте addSuppressed для ошибок при очистке, либо полагайтесь на try-with-resources.
- Централизованный слой трансляции ошибок (например, контроллер → сервис → dao): ловим на границе и мапим на понятные ответы/статусы.
Как правильно восстанавливать состояние (общие рекомендации)
- Всегда планируй компенсацию: для каждой операции, изменяющей состояние, имей шаги отката (rollback/compensating action).
- В async-коде гарантируй, что cleanup выполняется в finally/try-with-resources/whenComplete.
- Не делай cleanup, который может скрыть исходную ошибку: если cleanup бросает исключение, присоедини его как suppressed / cause, а не затирай первичную ошибку.
- Для долгих/распределённых процессов используйте идемпотентные операции или храните статус операции (например, durable saga), чтобы можно было корректно повторять/компенсировать.
Короткая сводка «что делать» при проектировании:
- Пробрасывай ошибки вверх с контекстом, не глуши их.
- Чистка ресурсов в finally / try-with-resources / whenComplete.
- Используй chainable cause/ suppressed для сохранения полной истории.
- Логируй на границах и добавляй correlation id.
- Для параллельных/асинхронных задач — явная обработка ошибок (allSettled, whenComplete), unwrap CompletionException/Promise rejections.
- Для retry/отката — проверяй идемпотентность и имей компенсации.
Если нужно — могу дать конкретные шаблоны/классы ошибок для вашего проекта (JS или Java) и примеры интеграции с логированием и корреляцией.