Как красиво написать чистый асинхронный код с обработкой ошибок
Мы все еще можем рассматривать JavaScript как унаследованный язык 90-х годов, но он считается самым крутым ребенком на рынке.
С одной стороны, важно обрабатывать ошибки, иначе пользователи столкнутся с препятствием, когда они не смогут изменить свои вводимые данные и не будут знать, что происходит, потому что сообщения об ошибках очень расплывчаты.
С другой стороны, наличие огромной кодовой базы с множеством условных выражений и объектов ошибок, распространяющихся повсюду, как дикие животные, нежелательно для команды разработчиков.
Но давайте будем честными, обрабатывать каждую возможную ошибку сложно.
Итак, здесь, в этом сообщении блога, я покажу вам некоторые способы эффективной обработки асинхронных ошибок в JavaScript.
В этой публикации я черпаю вдохновение в современных языках программирования, таких как Go и Swift, и пропущу объяснения async/await
, Promise
и обратных вызовов.
Вдохновение
Такие языки, как Go, уделяют большое внимание обработке ошибок. Всегда есть переменная с именем err
, которую нужно проверить. Вот пример:
if data, err := datastore.Get(c, key, record); err != nil { http.Error(w, err.Error(), 500) return }
А вот пример на Swift:
func icon() -> UIImage {
guard let image = UIImage(named: "Photo") else {
return UIImage(named: "Default")!
}
return image
}
Моя цель - сделать что-то столь же красивое, краткое и понятное, как эти предыдущие примеры, но на JavaScript.
асинхронные методы
Вот несколько async
методов:
async function fetchData() { return fetch("..."); } async function sendData(data) { return fetch("...", { method: "post", body: JSON.stringify(data), }); } async function signData(data) { // Do something cool with cryptography ;) }
Мы будем использовать эти методы в следующих разделах.
Никакой обработки
Быстрое и грязное решение - пропустить try/catch
и пожелать лучшего. Если вы достаточно уверены в глобальной интернет-сети, своих серверах и пользователе (самом непонятом существе в мире), вы можете написать такой код:
async function handleClick() { const data = await fetchData(); const signed = await signData(data); const saved = await sendData(signed); setState({ status: "ok ✅" }); }
Целостный улов
Минимальное дополнение, которое вы должны написать, - это, по крайней мере, отображать предупреждение и, возможно, сообщать об ошибке с большой упаковкой try/catch
.
async function handleClick() { try { const data = await fetchData(); const signed = await signData(data); const saved = await sendData(signed); setState({ status: "ok ✅" }); } catch (err) { setState({ status: "Something bad happened 🤷♂️" }); } }
При таком подходе вы позволяете пользователю или системе повторить действие, но они никогда не узнают причину ошибки. Это может сильно расстраивать.
Пошаговая инструкция пусть / попробует / поймает
Вот более управляемый поток, в котором вы можете фактически сказать, где возникает ошибка.
async function handleClick() { let data = null; try { data = await fetchData(); } catch (err) { return setState({ status: "Network error 🔌" }); } let signed = null; try { signed = await signData(data); } catch (err) { return setState({ status: "Signature error 🔑" }); } try { const saved = await sendData(signed); setState({ status: "ok ✅" }); } catch (err) { return setState({ status: "Could not save data 💾" }); } }
Лично я говорю: «Я не большой поклонник let
и тем более var
». Итак, можем ли мы достичь того же результата, но с синтаксисом, подобным Go или Swift?
Моя рекомендация: лови как охранник
Можно иметь синтаксис, аналогичный защитным предложениям в JavaScript, если мы перехватим ошибку, обработаем ее с любым требуемым побочным эффектом, а затем выдадим удобную для пользователя ошибку, которую мы можем отобразить позже пользователю. .
async function handleClick() { try { const data = await fetchData().catch(err => { throw new Error("Network error 🔌"); }); const signed = await signData(data).catch(err => { throw new Error("Signature error 🔑"); }); const saved = await sendData(signed).catch(err => { // Report error or any side effect. throw new Error("Could not save data 💾"); }); setState({ status: "ok ✅" }); } catch (err) { setState({ status: err.message }); } }
Также можно пропустить упаковку try/catch
, но это оставит это обещание необработанным.
Это может не быть проблемой, поскольку код асинхронный. Кроме того, это не должно нарушать рендеринг любого приложения React.js или завершаться с кодом ошибки любого скрипта Node.js.
Вот пример использования оператора запятой (он короче, но мало кто об этом знает):
async function handleClick() { const data = await fetchData().catch(err => { throw (setState({ status: "Network error 🔌" }), err); }); const signed = await signData(data).catch(err => { throw (setState({ status: "Signature error 🔑" }), err); }); const saved = await sendData(signed).catch(err => { // Report error or any side effect. throw (setState({ status: "Could not save data 💾" }), err); }); setState({ status: "ok ✅" }); }
Возможно, вам потребуется отключить правило ESLint, чтобы использовать оператор запятой без предупреждений.
Фактически, вы можете использовать любое значение, объект и даже undefined
в JavaScript (см. MDN). Теоретически можно даже выкинуть результат setState
, который равен void
:
async function handleClick() { const data = await fetchData().catch(err => { throw setState({ status: "Network error 🔌" }); // throw void; }); // ... setState({ status: "ok ✅" }); }
На мой взгляд, с точки зрения совместимости и стандартизации лучше придерживаться нативного Error
object.
Еще одна причина, по которой вы не должны выдавать пустое значение, заключается в том, что вы теряете информацию об ошибке, поэтому отладка становится сложнее.
При написании этого поста я также подумал о том, чтобы вернуть массив, как это делает setState
, чтобы он больше походил на Go. Вот пример:
async function p(promise) { return promise .then(result => [result, null]) .catch(err => [null, err]); } async function handleClick() { const [data, err0] = await p(fetchData()); if (err0) { return setState({ status: "Network error 🔌" }); } const [signed, err1] = await p(signData()); if (err1) { return setState({ status: "Signature error 🔑" }); } // ... setState({ status: "ok ✅" }); }
В конце концов, есть много способов добиться одного и того же результата: поток программирования, который обрабатывает ошибки чистым, интуитивно понятным образом как для пользователя, так и для команды разработчиков.
Вы можете поиграть с каждым из этих вариантов и смешать их, чтобы получить то, что подходит вашей кодовой базе. Это также работает с серверными приложениями, такими как Express.js или Koa.js.
А пока я буду придерживаться метода catch as guard.