Предложение: try/catch как выражение (с ключевым словом raise)
Введение
Данное предложение предлагает новую возможность в JavaScript: использование конструкции try/catch/finally
в контексте выражения, возвращающего значение. Для этого вводится новое ключевое слово raise
, предназначенное для явного "выноса" значения из блока try
или catch
наружу. В итоге весь блок try/catch
может выступать как единое выражение, результат которого определяется вызовами raise
внутри него.
В классическом JavaScript конструкция try...catch
является оператором и не напрямую возвращает значение. Это означает, что для использования результата выполнения try/catch
в выражении приходится прибегать к дополнительным переменным или функциям. Новое же предложение делает синтаксис более выразительным и функциональным, позволяя писать код в декларативном стиле без лишних промежуточных шагов.
Ниже рассматриваются мотивация данного предложения, примеры использования, синтаксис и семантика ключевого слова raise
, потенциальные сложности и влияние на существующий код, а также сравнение с похожими возможностями в других языках. Предложение оформлено в формате объяснительного документа (explainer) для нулевого этапа TC39.
Мотивация
Основная мотивация – упростить и улучшить читаемость кода, связанного с обработкой ошибок, особенно в случаях, когда нужно присвоить переменной значение, полученное либо при успешном выполнении операции, либо при отлове исключения. В текущем JavaScript подобный паттерн требует многословного императивного кода:
let result; try { result = computeValue(x); } catch (e) { result = fallbackValue(e); }
В таком коде переменная result
объявляется заранее, а затем внутри try
/catch
ей присваиваются значения. Этот шаблон нарушает декларативность и поток чтения кода – вместо того, чтобы определить result
в месте вычисления, мы вынуждены сначала объявить ее, а потом изменять. Желательно, чтобы можно было получить значение из try/catch
прямо в месте вызова, как это делается с тернарными операторами или if/else
(если бы if
мог быть выражением).
Другой пример неудобства – использование результатов внутри других выражений. Сегодня, чтобы, например, вызвать функцию с аргументом, полученным через try/catch
, приходится либо выносить вычисление вне вызова, либо использовать Immediately-Invoked Function Expression (IIFE):
// Текущий подход без try-выражения: const finalResult = processData( (() => { try { return riskyOperation(); } catch { return defaultValue; } })() );
Такой код труднее читать и поддерживать. Если бы try/catch
был выражением, можно было бы писать гораздо яснее:
// С предлагаемым try-выражением: const finalResult = processData( try { raise riskyOperation(); } catch (e) { raise defaultValue; } );
Здесь результат try
-выражения сразу передается в функцию processData
, без дополнительных функций-обёрток. Код выглядит лаконичнее и очевиднее.
Помимо присваивания, выражение try
вписывается в идеологию более функционального стиля: можно использовать его внутри других выражений, комбинировать с тернарными операторами, &&
/||
логикой, шаблонными литералами и т.д. Это особенно ценно при сложных условиях, когда решение об значении переменной зависит как от булевых условий, так и от успешности выполнения какого-то кода.
Обработка ошибок становится частью выражения, а не отдельным "побочным эффектом" со стороны. Это может поощрять более консистентный стиль кодирования, где минимум изменяемых переменных и промежуточных шагов.
Наконец, стоит отметить интерес сообщества к подобным возможностям. В других языках и предложениях уже есть схожие решения. Например, сообщество Python обсуждало возможность так называемых "exception-catching expressions" – выражений с обработкой исключений встроенной, по аналогии с тернарными операторами peps.python.org. Хотя в Python это не было принято, сам факт появления PEP 463 показывает потребность разработчиков в более кратком синтаксисе для таких случаев. Java добавила в версии 13 выражения switch
с ключевым словом yield
для возврата значений из блоков case
, чтобы избавиться от шаблона с внешней переменной stackoverflow.com. Наше предложение в духе этой тенденции – предоставить JavaScript-разработчикам инструмент, делающий код с try/catch
менее шаблонным и более выразительным.
Примеры использования до и после
Чтобы лучше понять предлагаемое изменение, рассмотрим несколько пар примеров "до и после". Они иллюстрируют, как новый синтаксис с raise
сократит код и улучшит его понятность.
Присваивание с обработкой ошибок:
let config; try { config = loadConfig(); } catch { config = getDefaultConfig(); }
const config = try { raise loadConfig(); } catch { raise getDefaultConfig(); };
Теперь переменная config
объявляется как const
и сразу инициализируется результатом try
-выражения. В случае успеха вызова loadConfig()
значение "поднимается" наружу через raise
внутри блока try
. Если же случится исключение, блок catch
выполнит свой raise
с результатом getDefaultConfig()
. В итоге config
получит либо загруженную конфигурацию, либо конфигурацию по умолчанию – и все это в одном выразительном конструктиве.
Интеграция с условными операторами (if
/тернарный):
let userName; if (user.isLoggedIn) { try { userName = fetchUserName(user.id); } catch { userName = "Гость"; } } else { userName = "Не авторизован"; }
Этот код можно переписать с использованием выражений, вложив try
внутрь тернарного оператора.
const userName = user.isLoggedIn ? try { raise fetchUserName(user.id); } catch { raise "Гость"; } : "Не авторизован";
Логика осталась прежней, но запись стала короче. Вместо того чтобы объявлять userName
заранее и несколько раз присваивать, мы напрямую определяем ее через выражение. При залогиненом пользователе предпринимается попытка получить имя (в случае ошибки подставится "Гость"), а при отсутствующей авторизации сразу используется строка "Не авторизован". Код легко читается как единое выражение, описывающее правила вычисления значения.
Использование в async
функциях с await
:
Нововведение совместимо с асинхронным кодом. Ключевое слово raise
поддерживает await
для правильного получения промиса.
async function getData() { try { const response = await fetch(url); return process(response); } catch (err) { return getFallbackData(err); } }
async function getData() { return try { raise await fetch(url); } catch (err) { raise getFallbackData(err); }; }
Здесь мы сразу return
-им результат try
-выражения из функции. Внутри try
происходит await fetch(url)
, и полученный ответ подается в raise
– то есть либо возвращается как результат всей функции (если все прошло успешно), либо, при ошибке запроса, управление перейдет в блок catch
, который вызовет raise getFallbackData(err)
и вернет альтернативные данные. Обратите внимание: raise await
аналогичен комбинации await
+ return
для промиса – он дожидается результата и "поднимает" его наружу.
Эти примеры демонстрируют, как новая возможность устраняет шаблонное кодирование (boilerplate) и делает намерения разработчика более прозрачными. Вместо фрагментированного кода с объявлениями вне блоков и присвоениями внутри, мы получаем целостные выражения, которые проще воспринимать.
Синтаксис и семантика try-выражения с raise
Предлагаемое изменение включает два связанных элемента: разрешить конструкции try/catch/finally
возвращать значение, и ввести ключевое слово raise
для явного указания возвращаемого значения. Ниже описаны основные правила и поведение.
Основные правила и ограничения
Обязательность raise
в ветвях:
Если хотя бы одна из ветвей (try
или catch
) внутри конструкции использует raise
для возврата значения, то обе ветви должны содержать raise
. Другими словами, в выражении вида try { ... } catch { ... }
не допускается ситуация, когда try
возвращает значение через raise
, а в catch
при этом нет raise
(или наоборот). Такое требование гарантирует, что все возможные пути выполнения возвращают значение. Это схоже с тем, как в выражениях switch
в Java каждой ветке должно соответствовать возвращаемое значение (через ->
или yield
), иначе код считается неполным docs.oracle.comdocs.oracle.com. Если ни одна из ветвей не содержит raise
, конструкция try/catch
работает как обычный оператор и не производит значения (то есть не может использоваться там, где ожидается значение).
Возврат значения из блока try
/catch
:
Ключевое слово raise
действует по аналогии с return
, но не на уровне функции, а на уровне самого блока try/catch
. Когда выполнение кода встречает raise <выражение>
, текущий блок (try
или catch
) немедленно завершается, и указанное значение "поднимается" наружу, становясь результатом всего выражения try/catch
. Код после вызова raise
в том же блоке не выполняется (как и в случае с return
внутри функции). Например:
const value = try { if (conditionFailed) { raise null; // немедленный выход из try-блока с результатом null } // ... иначе продолжаем вычисления let result = compute(); raise result; // возвращаем вычисленное значение console.log("Этот код уже не исполнится, т.к. выше был raise"); } catch (e) { raise handleError(e); };
Здесь внутри try
предусмотрен ранний выход: при нарушении условия сразу возвращается null
. Если же условие в порядке, вычисляется result
и он возвращается. Любой код после raise
не выполнится. Блок catch
также обязательно возвращает значение (обработку ошибки) через raise
. Таким образом, переменная value
получит либо null
, либо корректно вычисленный результат, либо (если исключение не перехвачено данным catch) произойдет обычное распространение исключения вверх (см. ниже).
Блок catch
с raise
срабатывает только если в блоке try
было брошено исключение (с помощью обычного throw
или из-за ошибки). В этом случае выполнение переходит в catch
, и там зачастую пишется raise
с каким-то значением-заменой или обработкой. Если в catch
есть raise
, как требует правило, то при перехвате исключения именно его значение станет результатом всего try
-выражения. Если же исключение не произошо в try
, то блок catch
пропускается (как обычно) и не влияет на результат.
Конструкция может включать блок finally
так же, как обычный try/catch/finally
. Блок finally
выполняется всегда после try
/catch
– вне зависимости, было исключение или нет. Однако, в контексте выражения важно, как finally
влияет на возвращаемое значение: по умолчанию, если в finally
ничего не предпринимать, результат, сформированный в try
или catch
через raise
, сохраняется. Но если внутри finally
будет вызван raise
, он переопределит ранее подготовленный результат. То есть raise
в finally
имеет приоритет: он заставляет всё выражение вернуть то значение, которое указано в finally
, даже если до этого в try
или catch
уже был выполнен raise
с другим значением. Это поведение аналогично тому, как в сегодняшнем JavaScript блок finally
может перезаписать результат функции, если внутри него сделать return
или выбросить новое исключение (что, кстати, считается нежелательной практикой, но технически возможно).
Доступ к поднятому значению в finally
:
Возникает вопрос – как finally
может узнать, какое значение было поднято из try
или catch
? Предлагается расширение синтаксиса: блок finally
может объявить псевдопараметр, например: finally (raised) { ... }
. Переменная (идентификатор) в скобках после ключевого слова finally
будет доступна внутри блока и содержать значение, переданное последним вызовом raise
в try
или catch
. Если исключение произошло, а catch
поднял значение, raised
будет равно этому значению; если исключения не было и try
поднял значение – соответственно тому. Если ни один из блоков не вызывал raise
(то есть на самом деле выражение не вернуло ничего), raised
может быть undefined
. Это дает возможность в finally
например логгировать или модифицировать результат перед окончательным возвратом. Блок finally
при этом не обязан вызывать raise
– он может просто выполнить побочные действия. Но если внутри finally
все же вызвать raise
, то новое значение замещает собой прежнее. Пример:
const data = try { raise fetchData(); } catch (e) { raise null; } finally (result) { console.log("Результат перед выходом:", result); if (result === null) { // если данные не получены, переопределим результат на заглушку raise { status: "empty" }; } // иначе не вызываем raise, и значение из try/catch пройдет далее };
В этом коде мы всегда логируем полученный результат (успешный или null
при ошибке). Затем, если результат равен null
(т.е. произошла ошибка и catch
вернул null), мы решаем переопределить итоговый результат на объект { status: "empty" }
посредством raise
в finally
. Если же результат не null
, finally
ничего не возвращает, и значение из try
(результат fetchData()
) пройдет наружу. Таким образом, data
окажется либо объектом с данными (при успешном запросе), либо объектом { status: "empty" }
(если произошла ошибка).
Предполагается ввести новое ключевое слово raise
в язык. Оно выступает как управляющая конструкция, не как функция. С точки зрения грамматики, raise
будет аналогично return
или throw
– за ним должно следовать выражение (или семантически допустимо опустить выражение, чтобы вернуть undefined
, как return;
). Использование raise
вне контекста блока try/catch
(который предназначен для возвращения значения) будет синтаксической ошибкой. Также нельзя использовать raise
внутри вложенных функций, генераторов или arrow-функций вместо return
– его действие ограничено именно текущим try
-выражением. Если попытаться написать:
function f() { try { raise 42; } catch { raise -1; } // ... return 0; }
то поведение будет следующим: когда f()
вызвана, внутри нее выполнится try
-выражение. Столкнувшись с raise 42
, оно завершит это try
-выражение и вернет 42 наружу – в нашем случае, внутрь из функции f()
, но так как результат выражения не был присвоен и использован, выполнение дойдет до return 0 и
f()
вернет 0. Здесь важно разграничить: return
выходит из функции, throw
выходит из функции (если не пойман), а raise
выходит только из блока try/catch
-выражения. Это подобно тому, как оператор yield
в Java вызывает выход лишь из switch
-выражения, а не из окружающей функции stackoverflow.com.
Допустимые возвращаемые значения:
Вызов raise
может "поднять" любое значение. Это может быть примитив (число, строка, и т.д.), объект, null
или undefined
. Нет ограничений или специальных требований, как к throw
(который зачастую используют с объектами Error, но формально тоже может бросить что угодно). raise
просто берет указанное выражение и делает его результатом. Даже raise undefined;
считается корректным – это явный способ вернуть "ничего". Если raise
написать без выражения (как отдельную инструкцию raise;
), по аналогии с return;
это будет означать поднять undefined
.
Как показано ранее, raise
полноценно работает в сочетании с await
внутри async
-функций. При использовании await
результат промиса будет получен, а затем передан наружу. Это эквивалентно тому, как return await
обрабатывает значение: сначала ждет, затем возвращает его. Важно подчеркнуть: если внутри try
используется await
, то и сам try
-блок становится асинхронным, но поскольку весь код находится внутри async
функции, это естественно. Нет дополнительных ограничений – можно писать raise await somePromise;
. Если обещание (промис) разрешится нормально, его значение станет результатом, если отклонится (throw внутри промиса), то будет брошено исключение и, возможно, перехвачено блоком catch
данного выражения.
Правила области видимости (scoping)
Ключевое слово raise
не вводит новой области видимости – оно действует внутри уже существующей области того блока, где находится. Переменные, объявленные внутри try
или catch
(через let
/const
), видимы до конца этого блока (включая до его закрывающей фигурной скобки). Вызов raise
может использовать любую переменную, доступную в данной области (как локальную, так и внешнюю). Однако после выполнения raise
дальнейшие инструкции блока не выполняются, что важно учитывать: например, если ниже по коду определена еще какая-то переменная, она никогда не будет инициализирована, что эквивалентно ситуации с преждевременным return
. Это не создает новых проблем, аналогичная ситуация бывает с return
или throw
. Просто разработчику и инструментам нужно будет следить за тем, чтобы код после raise
не содержал критически важных операций (lint-правила могут подсвечивать "unreachable code after raise", подобно тому, как делают для return
).
Отдельно стоит сказать про Temporal Dead Zone (TDZ). Появление try
-выражений не меняет существующих правил TDZ для let
/const
. Но могут быть тонкости, если try
-выражение используется в месте объявления переменной. Рассмотрим пример:
// Неправильно: let value = try { raise someVar; } catch (e) { console.log(e); // ReferenceError: Cannot access 'someVar' before initialization raise 1; }; let someVar = 5;
Здесь мы пытаемся использовать someVar
внутри инициализации value
до того, как someVar
объявлена. Разработчикам нужно по-прежнему следить за порядком объявлений. В остальном try
-выражение не вводит новых ограничений: оно вычисляется последовательно, и любые переменные, объявленные внутри него, живут только в нем и не "просачиваются" наружу (наружу выходит лишь значение). Переменная, объявленная во внешней функции с тем же именем, что и идентификатор в finally (ident)
, не находится в конфликте – идентификатор в скобках finally
работает как параметр, подобно параметру функции или названному exception-параметру в catch.
Потенциальные сложности и влияние на экосистему
Внедрение нового синтаксиса – особенно с введением нового ключевого слова – требует внимательного отношения к деталям реализации и обратной
Внедрение нового синтаксиса – особенно с введением нового ключевого слова – требует внимательного отношения к деталям реализации и обратной совместимости.
- Обратная совместимость (ключевое слово
raise
): На сегодняшний день словоraise
не является зарезервированным в JavaScript. Это означает, что существующий код мог использоватьraise
как имя переменной, функции, свойства объекта и т.д. Введение его в качестве ключевого слова теоретически может сломать такой код. Например, если где-то определеноvar raise = 5;
или функцияfunction raise(x) { ... }
, то после появления ключевого слова это станет синтаксической ошибкой или будет восприниматься иначе. Чтобы минимизировать ущерб, можно рассмотреть контекстное ключевое слово: сделать так, чтоraise
распознается как ключевое только внутри конструкцийtry { ... } catch { ... } finally { ... }
, где по грамматике ожидается возврат значения. Подход с контекстными (или "ограниченными") ключевыми словами уже применялся: например,yield
в ES6 стал ключевым словом только внутри генераторов,await
– только внутриasync
функций или модулей. Вероятно,raise
может быть сделан зарезервированным в строгом режиме или только в специальных контекстах, чтобы не сломать существующие скрипты. Но в любом случае это обсуждаемо: возможно, придется выбрать другое слово или подход (альтернативы рассмотрим ниже). - Поддержка инструментов (lint, форматтеры, IDE): Появление нового синтаксиса потребует обновления инструментов разработки. Правила линтеров (ESLint и др.) должны будут научиться корректно парсить
try
как выражение и не ругаться, например, на присваивание вconst
черезtry { ... } catch { ... }
. Также могут появиться новые правила: например, предупреждение о коде послеraise
(как упоминалось), или требование, чтобы и в try, и в catch присутствовалraise
(это, скорее, синтаксический уровень, но линтер может помогать ловить такие ошибки понятнее). Форматтеры кода (Prettier и др.) тоже должны знать про новый синтаксис, чтобы правильно расставлять отступы. В средах вроде VSCode появятся подсветка и автодополнение. Это все рабочие моменты, которые сопровождают любую новую возможность языка. - Изменения в парсерах и AST: JavaScript-движки (V8, SpiderMonkey, JavaScriptCore) должны будут адаптировать парсер под новый конструктив. Появится необходимость распознавать конструкцию
try
как часть выражения. По спецификации, скорее всего, будет введено что-то вроде TryExpression. AST (Abstract Syntax Tree) форматы, такие как ESTree, тоже расширятся новым типом узла, напримерTryExpression
с узламиblock
,handler
(catch) иfinalizer
. Внутри него новые узлыRaiseExpression
для точек выхода. Инструменты, работающие с AST (бабели, трансформеры, minifiers) тоже потребуют обновления. Хотя изменение не тривиальное, оно локализовано: не затрагивает тонны существующей семантики, а лишь добавляет новый вид выражения и оператор. Близкий по масштабу пример – добавление опциональной цепочки (a?.b
), которое тоже потребовало внести новый тип узла. Сообщество обычно справляется с такими изменениями достаточно быстро, особенно если фича популярна. - Влияние на читаемость и соглашения: Хотя цель предложения – улучшить читаемость, у него могут быть и критики. Некоторые разработчики могут найти непривычным видеть
try/catch
внутри выражения. Будут вопросы стиля: можно ли злоупотреблять и создавать слишком громоздкие выражения? Будет ли код сложнее отладки, если в одной строке много всего, включая обработку ошибок? Эти проблемы не технические, но влияют на принятие. Вероятно, появятся рекомендации (в документации или сообществе), как уместно использовать try-выражения, а когда лучше оставить явныйtry/catch
блок. Например, правило: если логика обработки исключений сложная (больше нескольких строк), лучше не вкладывать ее в тернарный оператор, а писать отдельно. Или: избегать вложенных try-выражений ради читабельности. Эти аспекты находятся вне рамок спецификации, но важно иметь их в виду. - Производительность: В текущем виде
try/catch
в JavaScript несет незначительные издержки, особенно когда исключения не происходят. Введениеtry
-выражений сraise
скорее всего аналогично по затратам: пока не брошено исключение, выполнение идет линейно, с парой дополнительных проверок наraise
. Еслиraise
вызывается, это примерно то же, чтоreturn
из функции – мгновенное завершение блока. Возможно, реализация затронет механизм completion records в спецификации (уже сейчас у каждого блока есть значение завершения). Тонкая оптимизация может потребоваться, но ничего не указывает на серьезное падение производительности. Тем не менее, движкам надо будет обработать новые пути выхода (например, в JIT-компиляции), чтобы эффективно предсказывать и инлайнить такие вещи. Это работа оптимизаторов, но особых препятствий нет.
Аналоги в других языках
Идея делать конструкции вроде try/catch
вычисляемыми выражениями не нова. В разных языках реализовано разными путями:
Kotlin: В Kotlin блочный оператор try/catch
является выражением и может возвращать значение. Конструкция очень похожа на предлагаемую, но без специального ключевого слова – результатом считается последнее выражение, выполненное в блоке try
либо в блоке catch
kotlinlang.org. Например, в Kotlin:
val num = try { riskyOperation() } catch (e: Exception) { -1 }
Здесь num
получит либо результат riskyOperation()
, либо -1 в случае исключения. Блок finally
в Kotlin, хотя и выполняется всегда, не влияет на результат выражения (его используют только для побочных эффектов) kotlinlang.org. Наше предложение для JavaScript, по сути, стремится к аналогичному удобству, но более явным синтаксисом. Мы вводим raise
чтобы явно обозначать возвращаемое значение, вместо неявного "последнего выражения". Это решение принято из соображений ясности: явный ключевой слово снижает вероятность ошибки, когда, например, кто-то забыл, что надо вернуть значение, или случайно разместил ненужный код после нужного выражения.
Scala: В языке Scala try-catch-finally
также является выражением. Поскольку Scala – язык выражений, там практически все конструкции могут возвращать значение. Семантика как у Kotlin: результат – это либо значение из try
, либо из catch
. Отличие Scala в том, что catch
там основан на сопоставлении с образцом (pattern matching), но суть та же. Кроме того, в Scala имеется класс Try
(в библиотеке), представляющий результат выполнения, который может быть успешным (Success
) или содержать исключение (Failure
). Он позволяет писать код в функциональном стиле: вместо использования ключевых слов, оборачивает результат в объект. Например:
import scala.util.Try val resultTry = Try(riskyOperation()) // вернет Success(значение) или Failure(ошибка) val finalVal = resultTry.getOrElse(defaultValue)
Такой подход похож по цели – избавиться от явного try/catch при присваивании – но реализован через библиотечный класс. Наш путь – интегрировать удобство на уровне языка, без дополнительных объектов-оберток.
Rust: В Rust отсутствуют исключения, поэтому напрямую аналога try-catch
нет, но есть результатный тип Result
(и Option
) и оператор ?
для распространения ошибок. Интересно, что в версии Rust 2018+ появилось понятие try-блока (пока экспериментального, на момент появления) doc.rust-lang.org. Try-блок в Rust выглядит как:
let result: Result<T, E> = try { let x = doSomething()?; // если doSomething() вернет Err, то выходим из try-блока let y = doOther()?; // аналогично, ? оператор возвращает из блока при ошибке compute(x, y) };
Здесь, если внутри блока встречается ?
, он немедленно прерывает блок, возвращая Err(...)
как значение всего блока. Если же ни один ?
не сработал, результатом будет Ok(...)
с последним выражением (в примере compute(x,y)
). Таким образом, Rust решает сходную задачу – получить из блока либо успех, либо ошибку – но делая это через типизацию (Result) вместо исключений, а try {}
+ ?
служат синтаксическим сахаром. Для нас Rust интересен тем, что демонстрирует: блок-выражение с ранним выходом по специальному оператору – концепция не чуждая современным языкам. По аналогии, raise
в JavaScript – это "ранний выход" с готовым значением, не приводящий к выбросу исключения.
Python (PEP 463): Как упоминалось, в Python был предложен синтаксис для "exception-catching expressions". Идея заключалась в том, чтобы позволить конструкцию вида:
result = expr1 except SomeException: expr2
Это бы означало: вычислить expr1
, и если возникает SomeException
, вместо него взять значение expr2
. Предложение (PEP 463) обосновывалось тем, что в Python не хватает выражения для EAFP-стиля (Easier to Ask Forgiveness than Permission) прямо внутри других выражений peps.python.org. Например:
process(value if key in dict else default) # LBYL стиль (есть тернарный оператор) process(dict[key] except KeyError: default) # Предлагаемый EAFP стиль
Хотя PEP 463 был вежливо отклонен Guido ван Россумом (отчасти из опасений усложнения языка), он является показателем потребности. Однако предлагаемое решение в JS несколько отличается синтаксисом. Python-предложение реализовывало это более лаконично (буквально как инфиксный оператор except
внутри выражения). Мы же в JavaScript стараемся сохранить привычную форму try { } catch { }
даже в выражении, и вводим raise
вместо перегрузки существующего слова. Это выглядит более громоздко чем PEP 463, но лучше вписывается в синтаксис JS. Стоит отметить, что Python до сих пор не имеет никаких try-выражений — то есть разработчики по-прежнему используют обычные try/except блоки.
Другие языки: В ряде функциональных и скриптовых языков есть схожие механизмы. Например, OCaml/F# (ML-семейство) – там try...with
(эквивалент catch) является выражением, возвращающим значение, поскольку в этих языках вообще все конструкции возвращают значение. Haskell не имеет исключений как основного механизма, но есть монада Either
/IO
для обработки ошибок без выброса. Go пошел по пути вообще отказаться от исключений, возвращая ошибку как второй результат функции. В контексте JavaScript, где исключения есть и активно используются, наш подход ближе к Kotlin/Scala – сделать их использование более гибким и менее шаблонным.
Заключение
В этом документе был представлен набросок предложения для TC39, позволяющего использовать try/catch/finally
как выражение с возвращаемым значением, благодаря введению нового ключевого слова raise
. Мы рассмотрели мотивацию (упрощение шаблонного кода, улучшение декларативности), детально описали предполагаемый синтаксис и поведение raise
, включая правила его использования и взаимодействия с finally
. Также были обсуждены потенциальные подводные камни (ключевое слово, AST, линтеры, совместимость) и показано, что подобные возможности существуют или обсуждались в других языках (Kotlin, Scala, Rust, Python и др.), что подтверждает ценность идеи. Наконец, проанализированы альтернативные подходы и обосновано, почему выбран именно данный путь.
Призыв к обсуждению: На стадии 0 важно собрать мнения и выявить возможные проблемы, которые не покрыты черновиком. Открыты вопросы могут включать:
- Выбор ключевого слова: подходит ли
raise
по смыслу и нет ли риска путаницы с "raise exception" из других языков? - Нужен ли параметр в
finally
или можно обойтись без нового синтаксиса вродеfinally (raised)
? - Все ли случаи учтены (например, поведение при вложенных try-выражениях, при использовании
break/return
внутри блоков и т.п.)? - Насколько часто такая конструкция пригодится на практике, есть ли реальные примеры из кода, где это существенно улучшит качество?
Обсуждение этих вопросов, а также эксперименты с прототипированием (в Babel либо в отдельных ветках V8) помогут продвинуть предложение к Stage 1 и дальше, если сообщество посчитает идею полезной. Мы надеемся, что предоставленный explainer ясно передает суть и преимущества try
-выражений с raise
, и с нетерпением ждем обратной связи от TC39 и сообщества JavaScript разработчиков.
Ссылка на начальный драфт пропозала:
https://github.com/dmitrytarassov/tc39-raise-proposal-draft?tab=readme-ov-file