本文介紹最新的 ECMAScript 安全賦值運算子提案以及相應的替代實現
前言
我們經常會跟 try/catch
打交道,但如果你寫過 Go 或者 Rust 就會發現在這兩種語言中是沒有 try/catch
的,那麼這些語言怎麼進行錯誤捕獲呢
- Go: Error handling
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
- Rust:
? Operator
/Result
Type
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"));
}
- Swift:
try? Operator
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")
- Zig:
try Keyword
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.
上述函式的問題在於它可能會無聲失敗,可能導致程式崩潰而沒有任何明確的警告。
fetch
can reject.fetch
可以拒絕。json
can reject.json
可以拒絕。parse
can throw.parse
可以丟擲。- 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:
執行將遵循此順序:
getPromise[Symbol.result]()
might throw an error when called (if it's a synchronous function returning a promise).
getPromise[Symbol.result]()
可能會在被呼叫時丟擲錯誤(如果它是一個同步函式返回一個 Promise)。- If an error is thrown, it will be assigned to
error
, and execution will halt.
如果出現錯誤,它將被賦值為error
,並停止執行。 - If no error is thrown, the result will be assigned to
data
. Sincedata
is a promise and promises have aSymbol.result
method, it will be handled recursively.
如果未丟擲錯誤,結果將被賦值給data
。由於data
是一個 Promise,並且 Promise 有一個Symbol.result
方法,因此將遞迴處理。 - If the promise rejects, the error will be assigned to
error
, and execution will stop.
如果 Promise 被拒絕,錯誤將被賦值給error
,並且執行將停止。 - 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.
using
或 await 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
的管理流程僅在 error
為 null
或 undefined
時應用,且 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:
這種模式在許多語言的架構中都存在:
- Go
- Error Handling 錯誤處理
- Rust
?
Operator?
運算子Result
TypeResult
型別
- Swift
- The
try?
Operator 運算子try?
- The
- Zig
try
Keywordtry
關鍵詞
- And many others...
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 本提案不旨在解決的問題
- 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。 - 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:
雖然此提案仍處於初期階段,但我們已意識到幾個限制和需要進一步發展的領域:
- Nomenclature for
Symbol.result
Methods: We need to establish a term for objects and functions that implementSymbol.result
methods. Possible terms include Resultable or Errorable, but this needs to be defined.
Symbol.result
方法的命名:我們需要為實現Symbol.result
方法的物件和函式建立一個術語。可能的術語包括 Resultable 或 Errorable,但這需要定義。 - Usage of
this
: The behavior ofthis
within the context ofSymbol.result
has not yet been tested or documented. This is an area that requires further exploration and documentation.
this
的使用:在Symbol.result
的上下文中,this
的行為尚未經過測試或文件化。這是一個需要進一步探索和文件化的領域。 - Handling
finally
Blocks: There are currently no syntax improvements for handlingfinally
blocks. However, you can still use thefinally
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 作者
- Arthur Fiorette (Twitter)
Inspiration 靈感
- This tweet from @LeaVerou
- Effect TS Error Management
- The
tuple-it
npm package, which introduces a similar concept but modifies thePromise
andFunction
prototypes—an approach that is less ideal.
tuple-it
npm 包,引入了類似的概念但修改了Promise
和Function
原型——這種方法不太理想。 - 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.