Сравните модели обработки ошибок в JavaScript (исключения и промисы/async-await), Go (возврат error как значения) и Rust (Result/Option): приведите короткие примеры, обсудите влияние модели на композицию кода, читабельность, обработку ресурсов и тестируемость, и предложите рекомендации для публичного API библиотеки
Кратко: у каждой модели свои сильные стороны и компромиссы. Ниже — сравнение, короткие примеры и практические рекомендации для публичного API библиотеки.
1) JavaScript — исключения и Promises/async-await
Модель: синхронно бросают исключения throwthrowthrow, в асинхронном коде — Promise reject; async/await делает промисы похожими на синхронный код, но тип ошибок не контролируется языком (любой объект может быть "брошен").Преимущества: простая видимая non−localnon-localnon−local передача ошибки; async/await делает чтение и композицию асинхронного кода удобной.Минусы: отсутствие статической типизации ошибок; легко пропустить обработку; смешение контроля потока и ошибок.
Примеры:
Синхронно:
function parseJsonsss { if !s!s!s throw new Error"empty""empty""empty"; return JSON.parsesss; }
2) Go — возвращаем error как значение - Модель: функции возвращают value,errorvalue, errorvalue,error. Ошибка — обычное значение, которое нужно явно проверить iferr!=nilif err != niliferr!=nil. Паники используются редко для критических, не-обрабатываемых ситуаций. - Преимущества: явность, простота трассировки, легко написать код, надежно контролировать ветви ошибок; defer для гарантированной очистки ресурсов. - Минусы: много шаблонного кода проверкиошибокпроверки ошибокпроверкиошибок; композиция цепочек действий требует явного пробрасывания ошибок; отсутствие богатой системы типов ошибок ноестьwrappingиerrors.Is/Asно есть wrapping и errors.Is/Asноестьwrappingиerrors.Is/As. Пример:
3) Rust — Result и Option, возможен panic для критических ошибок - Модель: ошибки — часть типов: Result<T, E> или Option<T>. Пропагируются явным ? оператором или pattern matching; ошибки типизированы EможетбытьenumE может быть enumEможетбытьenum. - Преимущества: статическая типизация ошибок, безопасная композиция, лаконичная передача ошибок через ?, ресурсы управляются RAII/Drop очисткагарантированапривыходеизблокаочистка гарантирована при выходе из блокаочисткагарантированапривыходеизблока. - Минусы: требуется проектирование типов ошибок и преобразований From/IntoFrom/IntoFrom/Into, иногда verbose для очень простых случаев; learning curve. Пример:
fn read_file(path: &str) -> Result<String, std::io::Error> { let s = std::fs::read_to_stringpathpathpath?; // ? пробрасывает ошибку Oksss
}
match read_file("a.txt") { Oktexttexttext => { / ok / } Erreee => { / обработка / } }
Option:
fn find_userid:u32id: u32id:u32 -> Option { ... } if let Someuuu = find_userididid { ... } else { ... }
Влияние модели на композицию кода - JS: - Исключения дают "non-local" выход — удобно, но делает явную композицию труднее отслеживаемой. - Promises позволяют композицию .then/.catch,Promise.all.then/.catch, Promise.all.then/.catch,Promise.all, но обработка ошибок типово не проверяема по типу. - async/await упрощает последовательную композицию, но нужно помнить await и возможные забытые catch. - Go: - Явное возвращение ошибок упрощает понимание потока и контроль — композиция простая, но вербозная: надо явно проверять и пробрасывать ошибки во многих местах. - Для сложной композиции используются helper-функции/паттерны wrapper,middlewarewrapper, middlewarewrapper,middleware. - Rust: - Result + ? дают очень удобную композицию: можно писать последовательный код, не захламляя его проверками. - Комбинаторы map,andthenmap, and_thenmap,andthen и pattern matching позволяют выразительную композицию обработок. Влияние на читабельность - JS: при хорошем использовании async/await код читаем; но неявность ошибок и разнообразие форм string,Error,objectstring, Error, objectstring,Error,object ухудшают понимание контракта. - Go: явность делает причины ошибок очевидными, но много boilerplate. - Rust: при грамотном дизайне типов ошибок код и контракт очень читаемы; синтаксис ? уменьшает шум. Обработка ресурсов cleanupcleanupcleanup
- JS: - try/finally и finally в async/await используются для очистки; в промисах нужен .finally. - GC управляет памятью, но внешние ресурсы надо освобождать вручную. - Go: - defer — сильный и надёжный инструмент для очистки внезависимостиотраннегоreturnвне зависимости от раннего returnвнезависимостиотраннегоreturn. - Rust: - RAII/Drop — ресурсы автоматически освобождаются при выходе из области видимости; это самый надёжный способ управления ресурсами. Тестируемость - JS: - Тестирование ошибок затрудняется отсутствием контрактов типов ошибок; нужно проверять по классу Error, message или пользовательской метке. - Легко писать интеграционные тесты с промисами/async-await. - Go: - Явные ошибки легко мокать и проверять; можно определить sentinel errors и сравнивать через errors.Is. - Rust: - Типизированные ошибки упрощают написание unit-тестов; можно моделировать варианты Result/Option в тестах прямо на уровне типов. Рекомендации для публичного API библиотеки общиепринципыобщие принципыобщиепринципы
- Документируйте все возможные ошибки классы/коды/семантикуклассы/коды/семантикуклассы/коды/семантику и указывайте, какие ошибки считаются временными/повторимыми. - Выбирайте модель ошибок, идиоматичную для языка неизобретайтечужиепаттерныне изобретайте чужие паттернынеизобретайтечужиепаттерны: - JS: возвращайте Promise asyncfunctionsasync functionsasyncfunctions и бросайте/отклоняйте с Error-подклассами; не бросайте примитивы; экспортируйте собственные Error-классы или коды err.codeerr.codeerr.code для машинной обработки. - Go: возвращайте T,errorT, errorT,error; не используйте panic для обычных ошибок; предоставьте хорошо именованные ошибки varErrNotFound=errors.New(...)var ErrNotFound = errors.New(...)varErrNotFound=errors.New(...) и используйте wrapping fmt.Errorf("fmt.Errorf("%w", err)fmt.Errorf(" чтобы сохранить трассировку; поддерживайте context.Context для отмены/таймаутов. - Rust: возвращайте Result<T, E> с E — понятный enum ошибки; реализуйте std::error::Error + Display; используйте thiserror/snafu для удобства; для простоты можно в высокоуровневых приложениях использовать anyhow, но публичный API лучше с явным enum. - Категоризация ошибок: предоставляйте классификацию например,NotFound,InvalidArgument,Timeout,Temporaryнапример, NotFound, InvalidArgument, Timeout, Temporaryнапример,NotFound,InvalidArgument,Timeout,Temporary и/или машинные коды, чтобы пользователи могли программно принимать решения retry,fallbackretry, fallbackretry,fallback. - Не смешивайте подходы: в одной библиотеке лучше единообразие например,вJSневозвращатьиногдапромис,иногдаколбэкнапример, в JS не возвращать иногда промис, иногда колбэкнапример,вJSневозвращатьиногдапромис,иногдаколбэк. - Ресурсная безопасность: обеспечьте гарантированный cleanup вJS—документацияпроfinally,вGo—предлагаемdefer,вRust—опишитеownershipиDropв JS — документация про finally, в Go — предлагаем defer, в Rust — опишите ownership и DropвJS—документацияпроfinally,вGo—предлагаемdefer,вRust—опишитеownershipиDrop. - Совместимость и миграция: если библиотека может быть использована из разных контекстов sync/asyncsync/asyncsync/async, документируйте поведение, и по возможности предоставьте только асинхронный API вJSв JSвJS или синхронный вGo/Rustв Go/RustвGo/Rust как это ожидаемо. - Тестируемость: проектируйте ошибки так, чтобы в тестах было легко симулировать различные ветви mock−объекты,возвратспециальныхerror−кодов,featureflagsmock-объекты, возврат специальных error-кодов, feature flagsmock−объекты,возвратспециальныхerror−кодов,featureflags. Конкретные советы по языкам для публичного API - JavaScript: - API должен возвращать Promise илибытьasyncили быть asyncилибытьasync. Все ошибки — экземпляры Error предпочтительнособственныеклассы,напримерMyLibErrorсполямиcode,metaпредпочтительно собственные классы, например MyLibError с полями code, metaпредпочтительнособственныеклассы,напримерMyLibErrorсполямиcode,meta. - Документируйте коды ошибок и условия бросания. - Пример: export class NotFoundError extends Error { constructormsgmsgmsg{ supermsgmsgmsg; this.code='NOT_FOUND'; } } - Go: - Возвращайте T,errorT, errorT,error. Определите пакетовую ошибку-константу для часто проверяемых случаев ErrNotFoundErrNotFoundErrNotFound. - Используйте errors.Is/errors.As для распознавания обёрнутых ошибок. - Не паниковать при обычных ошибках; используйте context для отмены. - Rust: - Возвращайте Result<T, ErrorEnum>. Сделайте ErrorEnum исчерпывающим и документируйте варианты. - Реализуйте Display и std::error::Error; предоставьте From<...> для удобных конверсий. - Для клиентов, которым важна удобность, можно в дополнение иметь модуль с высокоуровневым типом anyhowanyhowanyhow — но публичный API лучше с явными ошибками. Короткое резюме - Если язык динамический JSJSJS — используйте стандартную модель throw/rejectthrow/rejectthrow/reject но централизуйте ошибки в Error-подклассах с кодами/метаданными. - Если язык у вас явного error-as-value GoGoGo — следуйте идиоме: возвращайте error, используйте wrapping/Is и defer для ресурсов. - Если язык типизированный как Rust — отдавайте предпочтение Result/Option с хорошо продуманными типами ошибок и ? для удобства композиции. Если нужно, могу: - Предложить шаблон скелетскелетскелет enum-ошибок для Rust или пример Error-классов и кодов для JS; - Показать пример API библиотеки с конкретной схемой кодов ошибок и обработкой в типичном consumer-коде.
Кратко: у каждой модели свои сильные стороны и компромиссы. Ниже — сравнение, короткие примеры и практические рекомендации для публичного API библиотеки.
1) JavaScript — исключения и Promises/async-await
Модель: синхронно бросают исключения throwthrowthrow, в асинхронном коде — Promise reject; async/await делает промисы похожими на синхронный код, но тип ошибок не контролируется языком (любой объект может быть "брошен").Преимущества: простая видимая non−localnon-localnon−local передача ошибки; async/await делает чтение и композицию асинхронного кода удобной.Минусы: отсутствие статической типизации ошибок; легко пропустить обработку; смешение контроля потока и ошибок.Примеры:
Синхронно:function parseJsonsss {
if !s!s!s throw new Error"empty""empty""empty";
return JSON.parsesss;
}
try {
- Асинхронно async/awaitasync/awaitasync/await:const obj = parseJsonsss;
} catch errerrerr {
// обработка
}
async function fetchDataurlurlurl {
const res = await fetchurlurlurl;
if !res.ok!res.ok!res.ok throw new Error("fetch failed");
return res.json;
}
try {
- Промисы цепочки,композицияцепочки, композицияцепочки,композиция:const data = await fetchDataurlurlurl;
} catch errerrerr {
// обработка
}
Promise.all[fetchA(),fetchB()][fetchA(), fetchB()][fetchA(),fetchB()] .then(([a,b]) => ...)
2) Go — возвращаем error как значение.catch(err => ...);
- Модель: функции возвращают value,errorvalue, errorvalue,error. Ошибка — обычное значение, которое нужно явно проверить iferr!=nilif err != niliferr!=nil. Паники используются редко для критических, не-обрабатываемых ситуаций.
- Преимущества: явность, простота трассировки, легко написать код, надежно контролировать ветви ошибок; defer для гарантированной очистки ресурсов.
- Минусы: много шаблонного кода проверкиошибокпроверки ошибокпроверкиошибок; композиция цепочек действий требует явного пробрасывания ошибок; отсутствие богатой системы типов ошибок ноестьwrappingиerrors.Is/Asно есть wrapping и errors.Is/Asноестьwrappingиerrors.Is/As.
Пример:
func ReadFilepathstringpath stringpathstring []byte,error[]byte, error[]byte,error {
f, err := os.Openpathpathpath if err != nil {
return nil, err
}
defer f.Close return io.ReadAllfff }
data, err := ReadFile("a.txt")
3) Rust — Result и Option, возможен panic для критических ошибокif err != nil {
// обработка
}
- Модель: ошибки — часть типов: Result<T, E> или Option<T>. Пропагируются явным ? оператором или pattern matching; ошибки типизированы EможетбытьenumE может быть enumEможетбытьenum.
- Преимущества: статическая типизация ошибок, безопасная композиция, лаконичная передача ошибок через ?, ресурсы управляются RAII/Drop очисткагарантированапривыходеизблокаочистка гарантирована при выходе из блокаочисткагарантированапривыходеизблока.
- Минусы: требуется проектирование типов ошибок и преобразований From/IntoFrom/IntoFrom/Into, иногда verbose для очень простых случаев; learning curve.
Пример:
fn read_file(path: &str) -> Result<String, std::io::Error> {
let s = std::fs::read_to_stringpathpathpath?; // ? пробрасывает ошибку
Oksss }
match read_file("a.txt") {
Option:Oktexttexttext => { / ok / }
Erreee => { / обработка / }
}
fn find_userid:u32id: u32id:u32 -> Option { ... }
if let Someuuu = find_userididid { ... } else { ... }
Влияние модели на композицию кода
- JS:
- Исключения дают "non-local" выход — удобно, но делает явную композицию труднее отслеживаемой.
- Promises позволяют композицию .then/.catch,Promise.all.then/.catch, Promise.all.then/.catch,Promise.all, но обработка ошибок типово не проверяема по типу.
- async/await упрощает последовательную композицию, но нужно помнить await и возможные забытые catch.
- Go:
- Явное возвращение ошибок упрощает понимание потока и контроль — композиция простая, но вербозная: надо явно проверять и пробрасывать ошибки во многих местах.
- Для сложной композиции используются helper-функции/паттерны wrapper,middlewarewrapper, middlewarewrapper,middleware.
- Rust:
- Result + ? дают очень удобную композицию: можно писать последовательный код, не захламляя его проверками.
- Комбинаторы map,andthenmap, and_thenmap,andt hen и pattern matching позволяют выразительную композицию обработок.
Влияние на читабельность
- JS: при хорошем использовании async/await код читаем; но неявность ошибок и разнообразие форм string,Error,objectstring, Error, objectstring,Error,object ухудшают понимание контракта.
- Go: явность делает причины ошибок очевидными, но много boilerplate.
- Rust: при грамотном дизайне типов ошибок код и контракт очень читаемы; синтаксис ? уменьшает шум.
Обработка ресурсов cleanupcleanupcleanup - JS:
- try/finally и finally в async/await используются для очистки; в промисах нужен .finally.
- GC управляет памятью, но внешние ресурсы надо освобождать вручную.
- Go:
- defer — сильный и надёжный инструмент для очистки внезависимостиотраннегоreturnвне зависимости от раннего returnвнезависимостиотраннегоreturn.
- Rust:
- RAII/Drop — ресурсы автоматически освобождаются при выходе из области видимости; это самый надёжный способ управления ресурсами.
Тестируемость
- JS:
- Тестирование ошибок затрудняется отсутствием контрактов типов ошибок; нужно проверять по классу Error, message или пользовательской метке.
- Легко писать интеграционные тесты с промисами/async-await.
- Go:
- Явные ошибки легко мокать и проверять; можно определить sentinel errors и сравнивать через errors.Is.
- Rust:
- Типизированные ошибки упрощают написание unit-тестов; можно моделировать варианты Result/Option в тестах прямо на уровне типов.
Рекомендации для публичного API библиотеки
общиепринципыобщие принципыобщиепринципы - Документируйте все возможные ошибки классы/коды/семантикуклассы/коды/семантикуклассы/коды/семантику и указывайте, какие ошибки считаются временными/повторимыми.
- Выбирайте модель ошибок, идиоматичную для языка неизобретайтечужиепаттерныне изобретайте чужие паттернынеизобретайтечужиепаттерны:
- JS: возвращайте Promise asyncfunctionsasync functionsasyncfunctions и бросайте/отклоняйте с Error-подклассами; не бросайте примитивы; экспортируйте собственные Error-классы или коды err.codeerr.codeerr.code для машинной обработки.
- Go: возвращайте T,errorT, errorT,error; не используйте panic для обычных ошибок; предоставьте хорошо именованные ошибки varErrNotFound=errors.New(...)var ErrNotFound = errors.New(...)varErrNotFound=errors.New(...) и используйте wrapping fmt.Errorf("fmt.Errorf("%w", err)fmt.Errorf(" чтобы сохранить трассировку; поддерживайте context.Context для отмены/таймаутов.
- Rust: возвращайте Result<T, E> с E — понятный enum ошибки; реализуйте std::error::Error + Display; используйте thiserror/snafu для удобства; для простоты можно в высокоуровневых приложениях использовать anyhow, но публичный API лучше с явным enum.
- Категоризация ошибок: предоставляйте классификацию например,NotFound,InvalidArgument,Timeout,Temporaryнапример, NotFound, InvalidArgument, Timeout, Temporaryнапример,NotFound,InvalidArgument,Timeout,Temporary и/или машинные коды, чтобы пользователи могли программно принимать решения retry,fallbackretry, fallbackretry,fallback.
- Не смешивайте подходы: в одной библиотеке лучше единообразие например,вJSневозвращатьиногдапромис,иногдаколбэкнапример, в JS не возвращать иногда промис, иногда колбэкнапример,вJSневозвращатьиногдапромис,иногдаколбэк.
- Ресурсная безопасность: обеспечьте гарантированный cleanup вJS—документацияпроfinally,вGo—предлагаемdefer,вRust—опишитеownershipиDropв JS — документация про finally, в Go — предлагаем defer, в Rust — опишите ownership и DropвJS—документацияпроfinally,вGo—предлагаемdefer,вRust—опишитеownershipиDrop.
- Совместимость и миграция: если библиотека может быть использована из разных контекстов sync/asyncsync/asyncsync/async, документируйте поведение, и по возможности предоставьте только асинхронный API вJSв JSвJS или синхронный вGo/Rustв Go/RustвGo/Rust как это ожидаемо.
- Тестируемость: проектируйте ошибки так, чтобы в тестах было легко симулировать различные ветви mock−объекты,возвратспециальныхerror−кодов,featureflagsmock-объекты, возврат специальных error-кодов, feature flagsmock−объекты,возвратспециальныхerror−кодов,featureflags.
Конкретные советы по языкам для публичного API
- JavaScript:
- API должен возвращать Promise илибытьasyncили быть asyncилибытьasync. Все ошибки — экземпляры Error предпочтительнособственныеклассы,напримерMyLibErrorсполямиcode,metaпредпочтительно собственные классы, например MyLibError с полями code, metaпредпочтительнособственныеклассы,напримерMyLibErrorсполямиcode,meta.
- Документируйте коды ошибок и условия бросания.
- Пример: export class NotFoundError extends Error { constructormsgmsgmsg{ supermsgmsgmsg; this.code='NOT_FOUND'; } }
- Go:
- Возвращайте T,errorT, errorT,error. Определите пакетовую ошибку-константу для часто проверяемых случаев ErrNotFoundErrNotFoundErrNotFound.
- Используйте errors.Is/errors.As для распознавания обёрнутых ошибок.
- Не паниковать при обычных ошибках; используйте context для отмены.
- Rust:
- Возвращайте Result<T, ErrorEnum>. Сделайте ErrorEnum исчерпывающим и документируйте варианты.
- Реализуйте Display и std::error::Error; предоставьте From<...> для удобных конверсий.
- Для клиентов, которым важна удобность, можно в дополнение иметь модуль с высокоуровневым типом anyhowanyhowanyhow — но публичный API лучше с явными ошибками.
Короткое резюме
- Если язык динамический JSJSJS — используйте стандартную модель throw/rejectthrow/rejectthrow/reject но централизуйте ошибки в Error-подклассах с кодами/метаданными.
- Если язык у вас явного error-as-value GoGoGo — следуйте идиоме: возвращайте error, используйте wrapping/Is и defer для ресурсов.
- Если язык типизированный как Rust — отдавайте предпочтение Result/Option с хорошо продуманными типами ошибок и ? для удобства композиции.
Если нужно, могу:
- Предложить шаблон скелетскелетскелет enum-ошибок для Rust или пример Error-классов и кодов для JS;
- Показать пример API библиотеки с конкретной схемой кодов ошибок и обработкой в типичном consumer-коде.