ECMAScript 安全赋值运算符 (?=) 提案介绍及其 Polyfill

  • ECMAScript 安全赋值运算符 (?=) 提案介绍及其 Polyfill已关闭评论
  • 22 次浏览
  • A+
所属分类:Web前端
摘要

本文介绍最新的 ECMAScript 安全赋值运算符提案以及相应的替代实现我们经常会跟 try/catch 打交道,但如果你写过 Go 或者 Rust 就会发现在这两种语言中是没有 try/catch 的,那么这些语言怎么进行错误捕获呢


本文介绍最新的 ECMAScript 安全赋值运算符提案以及相应的替代实现

前言

我们经常会跟 try/catch 打交道,但如果你写过 Go 或者 Rust 就会发现在这两种语言中是没有 try/catch 的,那么这些语言怎么进行错误捕获呢

f, err := os.Open("filename.ext") if err != nil {     log.Fatal(err) } // do something with the open *File f 
use std::num::ParseIntError;  fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {     let first_number = first_number_str.parse::<i32>()?;     let second_number = second_number_str.parse::<i32>()?;      Ok(first_number * second_number) }  fn print(result: Result<i32, ParseIntError>) {     match result {         Ok(n)  => println!("n is {}", n),         Err(e) => println!("Error: {}", e),     } }  fn main() {     print(multiply("10", "2"));     print(multiply("t", "2")); } 
func fetchData() -> Data? {     if let data = try? fetchDataFromDisk() { return data }     if let data = try? fetchDataFromServer() { return data }     return nil }  let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg") 
const parseU64 = @import("error_union_parsing_u64.zig").parseU64;  fn doAThing(str: []u8) !void {     const number = try parseU64(str, 10);     _ = number; // ... } 

好了,以上例子仅用于体现 JavaScript 语法的简单(笑

不过也确实写 try/catch 嵌套写烦了,偶然发现了这个 ?= 提案,眼前一亮,真的简洁直观🥰

介绍 `?=` 运算符

安全赋值运算符 (?=) 提案符旨在简化代码中的错误处理,使代码更易于阅读,特别是在处理可能失败或抛出错误的函数时。

当使用 ?= 运算符时,它会检查函数或操作是否成功。如果成功,它会返回结果。如果失败,它会返回错误信息而不会让程序崩溃。

直接上代码对比:

Before:

async function getData() {   try {     const res = await fetch('https://api.example.com')     try {       const data = await res.json()       return data     } catch (parseError){       console.error(parseError)     }   } catch (networkError) {     console.error(networkError)   } } 

After:

async function getData() {   const [netErr, res] ?= await fetch('https://api.example.com')    if (netErr) return console.error(netErr)    const [parseErr, data] ?= await res.json()    if (parseErr) return console.error(parseErr)    return data } 
  • 如果 fetch 操作成功, netErr 会是 null ,而 res 则是返回的数据
  • 如果 fetch 操作失败, netErr 会是具体错误信息 ,而 res 则是 null

可以看出,使用安全赋值运算符可以消除代码嵌套,使代码更加干净并且易读

具体实现细节可以查阅后文提案 Readme

为什么使用

  • 简化错误处理:不再需要编写复杂的 try-catch 块
  • 代码整洁:代码变得更加易于阅读和理解
  • 行为一致:提供了一种在代码的不同部分处理错误的一致方式

替代实现/库

毕竟这是一个非常早期的语法提案,目前可能没有运行环境支持这个运算符

取而代之的是我们可以使用相应的替代实现先凑合一下😹

tuple-it: 一个简单的从 Promise [error, data] 的捕捉器

TupleIt 是一个实用工具,旨在简化 JavaScript 中 async / await 操作的错误处理

它将 await 语句包裹在一个 [error, data] 元组中,能够轻松地判断 Promise 是否 reject 或 resolve,而无需使用嵌套的 try / catch

这不仅提高了代码可读性,还减轻了在 JavaScript 开发中最常见的错误之一 - Promise reject 的错误处理

如何使用

npm i tuple-it 

扩展 Promise 原型被认为是一种糟糕的实践

TupleIt 提供了一个 tuple-it/register 模块 ,用于扩展 Promise 原型:

import 'tuple-it/register'  async function work(promise: Promise<WorkData>) {   const [error, data] = await promise.tuple()    if (error) {     console.log('Operation failed!')     return false   }    console.log('Operation succeeded!')   return true } 

避免全局作用域污染

可以直接导入 t 函数( tuple 的别名):

import { t } from 'tuple-it'  const [error, data] = await t(someAsyncFunction()) 

await-to-js: Async await wrapper for easy error handling without try-catch

安装

npm i await-to-js 

使用

import to from 'await-to-js'; // If you use CommonJS (i.e NodeJS environment), it should be: // const to = require('await-to-js').default;  async function asyncFunctionWithThrow() {   const [err, user] = await to(UserModel.findById(1))   if (user) {     // do something   } } 

自行编写

其实看了上面两个库的源码发现代码非常少,无非是封装了一下 try/catch,我们完全可以在自己项目中的 Util 自行实现

并且上面两个库都是处理Promise相关异步操作的,同步操作如JSON.parse()无法处理,我参照提案的 polyfill 自己编写了如下实现:

async function ta<T, E = Error>(promise: Promise<T>) {   try {     const result = await promise     return [null, result] as [null, T]   } catch (error) {     return [error || new Error('Thrown error is falsy'), null] as [E, null]   } }  function to<T, E = Error>(fn: () => Promise<T>): Promise<[null, T] | [E, null]> function to<T, E = Error>(fn: () => T): [null, T] | [E, null] function to<T, E = Error>(fn: () => T | Promise<T>) {   try {     const result = fn()     // `isPromise` 函数可以使用 `.then` 检查或者 `Object.prototype.toString` 检查实现     if (isPromise(result)) return ta<T, E>(result)     return [null, result] as [null, T]   } catch (error) {     return [error || new Error('Thrown error is falsy'), null] as [E, null]   } } 

使用方式:

const [err, res] = await ta(fetch('http://domain.does.not.exist')) console.log(err) // err: TypeError: Failed to fetch console.log(res) // res: null  const [err, data] = to(() => JSON.parse('<>')) console.log(err) // err: SyntaxError: Unexpected token '<', "<>" is not valid JSON console.log(data) // data: null 

like-safe: Inline try-catch error handling

npm i like-safe 
const safe = require('like-safe')  // Sync const [res1, err1] = safe(sum)(2, 2) // => [4, null] const [res2, err2] = safe(sum)(2, 'two') // => [null, Error]  // Async const [res3, err3] = await safe(sumAsync)(2, 2) // => [4, null] const [res4, err4] = await safe(sumAsync)(2, 'two') // => [null, Error]  // Shortcut for Promises const [res5, err5] = await safe(sumAsync(2, 2)) // => [4, null] const [res6, err6] = await safe(sumAsync(2, 'two')) // => [null, Error]  function sum (a, b) {   const out = a + b    if (isNaN(out)) {     throw new Error('Invalid')   }    return out }  async function sumAsync (a, b) {   // (Same as returning a Promise due async)   return sum(a, b) } 

提案翻译:ECMAScript 安全赋值运算符提案

注意

This proposal will change to try-expressions as its a more idiomatic apporach to this problem. Read more on #4 and #5.

此提案将更改为其更具语义的方法,使用 try-expressions 。有关更多信息,请参阅 #4#5

This proposal introduces a new operator, ?= (Safe Assignment), which simplifies error handling by transforming the result of a function into a tuple. If the function throws an error, the operator returns [error, null]; if the function executes successfully, it returns [null, result]. This operator is compatible with promises, async functions, and any value that implements the Symbol.result method.

此提案引入了一个新的运算符, ?= (安全赋值),它通过将函数的结果转换为元组来简化错误处理。如果函数抛出错误,运算符返回 [error, null] ;如果函数执行成功,它返回 [null, result] 。此运算符与 Promise、异步函数以及实现 Symbol.result 方法的任何值兼容。

For example, when performing I/O operations or interacting with Promise-based APIs, errors can occur unexpectedly at runtime. Neglecting to handle these errors can lead to unintended behavior and potential security vulnerabilities.

例如,在执行 I/O 操作或与基于 Promise 的 API 交互时,可能会在运行时意外地发生错误。忽略错误处理可能导致意外行为和潜在的安全漏洞。

const [error, response] ?= await fetch("https://arthur.place") 

Motivation 动机

  • Simplified Error Handling: Streamline error management by eliminating the need for try-catch blocks.
    简化错误处理:通过消除 try-catch 块的需求来简化错误管理。
  • Enhanced Readability: Improve code clarity by reducing nesting and making the flow of error handling more intuitive.
    增强可读性:通过减少嵌套和使错误处理的流程更加直观,提高代码的清晰度。
  • Consistency Across APIs: Establish a uniform approach to error handling across various APIs, ensuring predictable behavior.
    API 间的一致性:在各种 API 中建立统一的错误处理方法,确保行为可预测。
  • Improved Security: Reduce the risk of overlooking error handling, thereby enhancing the overall security of the code.
    改进安全性:降低忽略错误处理的风险,从而提高代码的整体安全性。

How often have you seen code like this?
你见过这样的代码多少次了?

async function getData() {   const response = await fetch("https://api.example.com/data")   const json = await response.json()   return validationSchema.parse(json) } 

The issue with the above function is that it can fail silently, potentially crashing your program without any explicit warning.
上述函数的问题在于它可能会无声失败,可能导致程序崩溃而没有任何明确的警告。

  1. fetch can reject. fetch 可以拒绝。
  2. json can reject. json 可以拒绝。
  3. parse can throw. parse 可以抛出。
  4. Each of these can produce multiple types of errors.
    这些中的每一个都可以产生多种类型的错误。

To address this, we propose the adoption of a new operator, ?=, which facilitates more concise and readable error handling.
为了解决这个问题,我们提议采用一个新的操作符, ?= ,这使得错误处理更加简洁和易于阅读。

async function getData() {   const [requestError, response] ?= await fetch(     "https://api.example.com/data"   )    if (requestError) {     handleRequestError(requestError)     return   }    const [parseError, json] ?= await response.json()    if (parseError) {     handleParseError(parseError)     return   }    const [validationError, data] ?= validationSchema.parse(json)    if (validationError) {     handleValidationError(validationError)     return   }    return data } 

Please refer to the What This Proposal Does Not Aim to Solve section to understand the limitations of this proposal.
请参阅《此提案不旨在解决的问题》部分,以了解此提案的局限性。

Proposed Features 提议的特性

This proposal aims to introduce the following features:
此提案旨在引入以下功能:

Symbol.result

Any object that implements the Symbol.result method can be used with the ?= operator.
任何实现 Symbol.result 方法的对象都可以与 ?= 运算符一起使用。

function example() {   return {     [Symbol.result]() {       return [new Error("123"), null]     },   } }  const [error, result] ?= example() // Function.prototype also implements Symbol.result // const [error, result] = example[Symbol.result]()  // error is Error('123') 

The Symbol.result method must return a tuple, where the first element represents the error and the second element represents the result.
Symbol.result 方法必须返回一个元组,其中第一个元素表示错误,第二个元素表示结果。

Why Not data First?

The Safe Assignment Operator (?=)

The ?= operator invokes the Symbol.result method on the object or function on the right side of the operator, ensuring that errors and results are consistently handled in a structured manner.
?= 操作符调用操作符右侧对象或函数上的 Symbol.result 方法,确保错误和结果以结构化的方式一致处理。

const obj = {   [Symbol.result]() {     return [new Error("Error"), null]   }, }  const [error, data] ?= obj // const [error, data] = obj[Symbol.result]() 
function action() {   return 'data' }  const [error, data] ?= action(argument) // const [error, data] = action[Symbol.result](argument) 

The result should conform to the format [error, null | undefined] or [null, data].
结果应符合格式 [error, null | undefined][null, data]

Usage in Functions

When the ?= operator is used within a function, all parameters passed to that function are forwarded to the Symbol.result method.
当在函数内部使用 ?= 运算符时,所有传递给该函数的参数都会被转发到 Symbol.result 方法。

declare function action(argument: string): string  const [error, data] ?= action(argument1, argument2, ...) // const [error, data] = action[Symbol.result](argument, argument2, ...) 
Usage with Objects

When the ?= operator is used with an object, no parameters are passed to the Symbol.result method.
当使用 ?= 操作符与对象一起使用时,不会向 Symbol.result 方法传递参数。

declare const obj: { [Symbol.result]: () => any }  const [error, data] ?= obj // const [error, data] = obj[Symbol.result]() 

Recursive Handling 递归处理

The [error, null] tuple is generated upon the first error encountered. However, if the data in a [null, data] tuple also implements a Symbol.result method, it will be invoked recursively.
[error, null] 元组在遇到第一个错误时生成。然而,如果 [null, data] 元组中的 data 也实现了 Symbol.result 方法,它将被递归调用。

const obj = {   [Symbol.result]() {     return [       null,       {         [Symbol.result]() {           return [new Error("Error"), null]         },       },     ]   }, }  const [error, data] ?= obj // const [error, data] = obj[Symbol.result]()  // error is  Error('string') 

These behaviors facilitate handling various situations involving promises or objects with Symbol.result methods:
这些行为有助于处理涉及 promises 或具有 Symbol.result 方法的对象的各种情况:

  • async function(): Promise<T>
  • function(): T
  • function(): T | Promise<T>

These cases may involve 0 to 2 levels of nested objects with Symbol.result methods, and the operator is designed to handle all of them correctly.
这些情况可能涉及 0 到 2 级嵌套对象,带有 Symbol.result 方法,操作符设计用于正确处理所有这些情况。

Promises

A Promise is the only other implementation, besides Function, that can be used with the ?= operator.
Promise 是除了 Function 之外唯一可以与 ?= 运算符一起使用的实现。

const promise = getPromise() const [error, data] ?= await promise // const [error, data] = await promise[Symbol.result]() 

You may have noticed that await and ?= can be used together, and that's perfectly fine. Due to the Recursive Handling feature, there are no issues with combining them in this way.
您可能已经注意到,可以同时使用 await?= ,这是完全没问题的。由于有递归处理功能,以这种方式组合它们没有任何问题。

const [error, data] ?= await getPromise() // const [error, data] = await getPromise[Symbol.result]() 

The execution will follow this order:
执行将遵循此顺序:

  1. getPromise[Symbol.result]() might throw an error when called (if it's a synchronous function returning a promise).
    getPromise[Symbol.result]() 可能会在被调用时抛出错误(如果它是一个同步函数返回一个 Promise)。
  2. If an error is thrown, it will be assigned to error, and execution will halt.
    如果出现错误,它将被赋值为 error ,并停止执行。
  3. If no error is thrown, the result will be assigned to data. Since data is a promise and promises have a Symbol.result method, it will be handled recursively.
    如果未抛出错误,结果将被赋值给 data 。由于 data 是一个 Promise,并且 Promise 有一个 Symbol.result 方法,因此将递归处理。
  4. If the promise rejects, the error will be assigned to error, and execution will stop.
    如果 Promise 被拒绝,错误将被赋值给 error ,并且执行将停止。
  5. If the promise resolves, the result will be assigned to data.
    如果 Promise 得到解决,结果将被赋值给 data

using Statement

The using or await using statement should also work with the ?= operator. It will perform similarly to a standard using x = y statement.
usingawait using 语句也应与 ?= 运算符一起工作。它将类似于标准的 using x = y 语句。

Note that errors thrown when disposing of a resource are not caught by the ?= operator, just as they are not handled by other current features.
请注意,当处理资源时抛出的错误不会被 ?= 操作符捕获,就像其他当前功能不会处理这些错误一样。

try {   using a = b } catch(error) {   // handle }  // now becomes using [error, a] ?= b  // or with async  try {   await using a = b } catch(error) {   // handle }  // now becomes await using [error, a] ?= b 

The using management flow is applied only when error is null or undefined, and a is truthy and has a Symbol.dispose method.
using 的管理流程仅在 errornullundefined 时应用,且 a 真实且具有 Symbol.dispose 方法。

Try/Catch Is Not Enough

The try {} block is rarely useful, as its scoping lacks conceptual significance. It often functions more as a code annotation rather than a control flow construct. Unlike control flow blocks, there is no program state that is meaningful only within a try {} block.
try {} 块很少有用,因为其作用域缺乏概念意义。它通常更多地作为代码注释而不是控制流程结构。与控制流程块不同, try {} 块内没有只在该块内有意义的程序状态。

In contrast, the catch {} block is actual control flow, and its scoping is meaningful and relevant.
相比之下, catch {} 块是实际的控制流程,其作用域是有意义且相关的。

Using try/catch blocks has two main syntax problems:
使用 try/catch 块有两个主要的语法问题:

// Nests 1 level for each error handling block async function readData(filename) {   try {     const fileContent = await fs.readFile(filename, "utf8")      try {       const json = JSON.parse(fileContent)        return json.data     } catch (error) {       handleJsonError(error)       return     }   } catch (error) {     handleFileError(error)     return   } }  // Declares reassignable variables outside the block, which is undesirable async function readData(filename) {   let fileContent   let json    try {     fileContent = await fs.readFile(filename, "utf8")   } catch (error) {     handleFileError(error)     return   }    try {     json = JSON.parse(fileContent)   } catch (error) {     handleJsonError(error)     return   }    return json.data } 

Why Not data First?

In Go, the convention is to place the data variable first, and you might wonder why we don't follow the same approach in JavaScript. In Go, this is the standard way to call a function. However, in JavaScript, we already have the option to use const data = fn() and choose to ignore the error, which is precisely the issue we are trying to address.
在 Go 中,约定是将数据变量放在首位,你可能会疑惑为什么我们在 JavaScript 中不遵循同样的方法。在 Go 中,这是调用函数的标准方式。然而,在 JavaScript 中,我们已经可以选择使用 const data = fn() 并选择忽略错误,这正是我们试图解决的问题。

If someone is using ?= as their assignment operator, it is because they want to ensure that they handle errors and avoid forgetting them. Placing the data first would contradict this principle, as it prioritizes the result over error handling.
如果有人使用 ?= 作为他们的赋值操作符,这是因为他们希望确保处理错误并避免忘记。将数据放在首位与此原则相矛盾,因为它优先考虑结果而不是错误处理。

// ignores errors! const data = fn()  // Look how simple it is to forget to handle the error const [data] ?= fn()  // This is the way to go const [error, data] ?= fn() 

If you want to suppress the error (which is different from ignoring the possibility of a function throwing an error), you can simply do the following:
如果你想抑制错误(这与忽略函数抛出错误的可能性不同),你可以简单地做以下操作:

// This suppresses the error (ignores it and doesn't re-throw it) const [, data] ?= fn() 

This approach is much more explicit and readable because it acknowledges that there might be an error, but indicates that you do not care about it.
这种方法更加明确和可读,因为它承认可能存在错误,但表示你并不关心这个错误。

The above method is also known as "try-catch calaboca" (a Brazilian term) and can be rewritten as:
上述方法也被称为“try-catch calaboca”,可以重写为:

let data try {   data = fn() } catch {} 

Complete discussion about this topic at #13 if the reader is interested.
#13 完整讨论此话题,如果读者感兴趣。

Polyfilling

This proposal can be polyfilled using the code provided at polyfill.js.
此提案可以通过提供的代码在 polyfill.js 处进行填充。

However, the ?= operator itself cannot be polyfilled directly. When targeting older JavaScript environments, a post-processor should be used to transform the ?= operator into the corresponding [Symbol.result] calls.
然而, ?= 运算符本身无法直接补全。在针对较旧的 JavaScript 环境时,应使用后处理程序将 ?= 运算符转换为相应的 [Symbol.result] 调用。

const [error, data] ?= await asyncAction(arg1, arg2) // should become const [error, data] = await asyncAction[Symbol.result](arg1, arg2) 
const [error, data] ?= action() // should become const [error, data] = action[Symbol.result]() 
const [error, data] ?= obj // should become const [error, data] = obj[Symbol.result]() 

Using ?= with Functions and Objects Without Symbol.result

If the function or object does not implement a Symbol.result method, the ?= operator should throw a TypeError.
如果函数或对象未实现 Symbol.result 方法, ?= 运算符应抛出 TypeError

Comparison 比较

The ?= operator and the Symbol.result proposal do not introduce new logic to the language. In fact, everything this proposal aims to achieve can already be accomplished with current, though verbose and error-prone, language features.
?= 操作符和 Symbol.result 提议并未向语言引入新的逻辑。实际上,这个提议想要实现的一切,都可以通过当前语言的现有特性来完成,尽管这些特性冗长且容易出错。

try {   // try expression } catch (error) {   // catch code }  // or  promise // try expression   .catch((error) => {     // catch code   }) 

is equivalent to: 相当于:

const [error, data] ?= expression  if (error) {   // catch code } else {   // try code } 

Similar Prior Art 相似的先前技术

This pattern is architecturally present in many languages:
这种模式在许多语言的架构中都存在:

While this proposal cannot offer the same level of type safety or strictness as these languages—due to JavaScript's dynamic nature and the fact that the throw statement can throw anything—it aims to make error handling more consistent and manageable.
虽然此提案无法提供与这些语言相同级别的类型安全或严格性——由于 JavaScript 的动态特性和 throw 语句可以抛出任何内容的事实——它旨在使错误处理更加一致和可管理。

What This Proposal Does Not Aim to Solve 本提案不旨在解决的问题

  1. Strict Type Enforcement for Errors: The throw statement in JavaScript can throw any type of value. This proposal does not impose type safety on error handling and will not introduce types into the language. It also will not be extended to TypeScript. For more information, see microsoft/typescript#13219.
    严格类型约束错误:JavaScript 中的 throw 语句可以抛出任何类型的值。此提案不为错误处理提供类型安全性,并不会将类型引入语言。它也不会扩展到 TypeScript。更多信息,请参见 microsoft/typescript#13219。
  2. Automatic Error Handling: While this proposal facilitates error handling, it does not automatically handle errors for you. You will still need to write the necessary code to manage errors; the proposal simply aims to make this process easier and more consistent.
    自动错误处理:虽然此提案简化了错误处理,但它并不会自动为您处理错误。您仍然需要编写必要的代码来管理错误;提案的目的是使这个过程更加容易和一致。

Current Limitations 当前限制

While this proposal is still in its early stages, we are aware of several limitations and areas that need further development:
虽然此提案仍处于初期阶段,但我们已意识到几个限制和需要进一步发展的领域:

  1. Nomenclature for Symbol.result Methods: We need to establish a term for objects and functions that implement Symbol.result methods. Possible terms include Resultable or Errorable, but this needs to be defined.
    Symbol.result 方法的命名:我们需要为实现 Symbol.result 方法的对象和函数建立一个术语。可能的术语包括 Resultable 或 Errorable,但这需要定义。
  2. Usage of this: The behavior of this within the context of Symbol.result has not yet been tested or documented. This is an area that requires further exploration and documentation.
    this 的使用:在 Symbol.result 的上下文中, this 的行为尚未经过测试或文档化。这是一个需要进一步探索和文档化的领域。
  3. Handling finally Blocks: There are currently no syntax improvements for handling finally blocks. However, you can still use the finally block as you normally would:
    处理 finally 块:目前没有语法改进来处理 finally 块。但是,您仍然可以像平常一样使用 finally 块:
try {   // try code } catch {   // catch errors } finally {   // finally code }  // Needs to be done as follows  const [error, data] ?= action()  try {   if (error) {     // catch errors   } else {     // try code   } } finally {   // finally code } 

Authors 作者

Inspiration 灵感

  • This tweet from @LeaVerou
  • Effect TS Error Management
  • The tuple-it npm package, which introduces a similar concept but modifies the Promise and Function prototypes—an approach that is less ideal.
    tuple-it npm 包,引入了类似的概念但修改了 PromiseFunction 原型——这种方法不太理想。
  • The frequent oversight of error handling in JavaScript code.
    JavaScript 代码中错误处理的频繁疏忽。

Reference

https://github.com/arthurfiorette/proposal-safe-assignment-operator

https://github.com/arthurfiorette/tuple-it

https://medium.com/@shahbishwa21/introduction-to-the-safe-assignment-operator-in-javascript-ddc35e87d37c

https://archive.is/20241030225804/https://medium.com/coding-beauty/new-javascript-operator-1e60dea05654


Also posted at https://www.nanoka.top/posts/5e83cb48/
fin.