搜尋
首頁web前端js教程TypeScript:錯誤管理的新領域

您正在開發 TypeScript 專案。程式碼乾淨並且架構良好,你為此感到自豪。有一天,彈出錯誤。它的堆疊追蹤比平均 npm 安裝更長,透過無數無法處理它的層向上冒泡。你的程式碼無法工作,你不知道從哪裡開始修復它,每一次嘗試都感覺像是笨拙的補丁。您的架構看起來不再那麼乾淨了。你討厭你的專案。您關掉電腦,去享受您的星期五

錯誤管理荒原

TypeScript: a new Frontier for Error Management

JavaScript 的錯誤管理 缺乏 Rust、Zig 和 Go 等現代語言提供的表達能力和開發人員體驗。它的動態特性和缺乏護欄常常讓開發人員面臨不確定性,而沒有更嚴格的平台提供的堅實基礎和保證。

軟體工程的這一重要支柱在語言的文化生態系統中沒有得到很好的體現,一些最受歡迎的npm 庫甚至沒有在其文件中提及異常。

缺乏標準會導致開發人員產生誤解異常很少發生。因此,這種扭曲的觀點導致社區對建立此類標準缺乏興趣。

Try-Catch:隱式的成本

JavaScript try-catch 模型隱藏了不明顯的意義。異常情況可能發生在任何地方,但預測它們卻極具挑戰性。這種看似簡單的模式常常掩蓋了日常程式碼中微妙的陷阱:

let value;
try {
  value = mayThrow();
} catch (e) {
  // Handle the exception.
}

程式碼片段中突出的第一個問題是範圍擴展,需要在 try-catch 區塊之外聲明變數以維持連續的控制流。這會導致更多冗長難以追蹤程式碼,隨著程式碼庫複雜性的增加,可能會引入微妙的錯誤。

這種動態錯誤處理的隱式性質增加了開發人員的認知負擔,要求他們在整個程式碼庫中在心裡追蹤異常來源。相較之下,顯式錯誤處理模型(例如 Go 中的模型)迫使開發人員承認並處理任何錯誤。

result, err := mayFail();

從長遠來看,這是一個巨大的勝利,隨著專案的發展,可以促進更順暢、更安全的維護。

除了這些挑戰之外,TypeScript 的 catch 子句在追蹤和嚴格鍵入可能拋出的錯誤的能力方面還存在不足,導致在最關鍵的地方失去類型安全性。 JavaScript 甚至允許拋出非錯誤值,這讓我們幾乎沒有任何保護措施。像 Rust 這樣的語言透過其錯誤處理設計展示了這種方法的強大功能和優雅:

match may_fail() {
  Ok(result) => println!("Success"),
  Err(Error::NotFound) => println!("Not found"),
  Err(Error::PermissionDenied) => println!("Permission denied"),
}

各種提案已提交給 TypeScript 團隊,旨在為更健全和可預測的異常系統奠定基礎。然而,這些提議經常被底層 JavaScript 平台中的限制所阻礙,該平台缺乏支援此類架構增強的必要原語。

同時,一些解決這些缺陷的提案也已提交給 TC39 (ECMAScript 標準化委員會),但仍處於早期考慮階段。正如馬特·波科克指出的那樣,宇宙的熱寂也在穩步進展。

尋求社區解決方案

TypeScript: a new Frontier for Error Management

當一種語言對創新產生摩擦時,開發者社群通常會以巧妙的函式庫和使用者態解決方案來回應。該領域目前的許多提案,例如特殊的Neverthrow,都從函數式程式設計
中汲取靈感,提供了一套類似於Rust 的結果類型的抽象和實用程式來解決問題:

function mayFail(): Result<string> {
  if (condition) {
    return err("failed");
  }

  return ok("value");
}
</string>

另一個突出的方法是效果
。這個強大的工具包不僅可以解決錯誤管理問題,還提供了一套全面的實用程式來處理非同步操作、資源管理等:

<script> // Detect dark theme var iframe = document.getElementById('tweet-1824338426058957098-986'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1824338426058957098&theme=dark" } </script>
import { Effect } from "effect";

function divide(a: number, b: number): Effect.Effect<number error> {
  return b === 0
    ? Effect.fail(new Error("Cannot divide by zero"))
    : Effect.succeed(a / b);
}

const result = Effect.runSync(divide(1, 2));
</number>

Outside the joy of a nerd like myself in digging into tech like this, adopting new technologies demands a careful cost-benefit analysis. The JavaScript ecosystem evolves at a breakneck pace, with libraries emerging and becoming obsolete in rapid succession.

Choosing the wrong abstraction can hold your code hostage, create friction in your development process, and demand blood, sweat, and tears to migrate away from. (Also, adding a new package is likely not gonna help with the 200mb bundle size of your React app.)

Error management is a pervasive concern that touches nearly every part of a codebase. Any abstraction that requires rethinking and rewriting such a vast expanse of code demands an enormous amount of trust—perhaps even faith—in its design.

Crafting a Path Forward

We've explored the limitations of user-land solutions, and life's too short to await commit approvals for new syntax proposals. Could there be a middle ground? What if we could push the boundaries of what's currently available in the language, creating something that aspires to be a new standard or part of the standard library, yet is written entirely in user-land and we can use it right now?

As we delve into this concept, let's consider some key principles that could shape our idea:

  • Conventions over Abstractions: Minimize abstractions by leveraging existing language features to their fullest.
  • Minimal API: Strive for simplicity without sacrificing functionality. Conciseness is often an indicator of robust and lasting design.
  • Compatibility and Integrability: Our solution shouldn't depend on universal adoption, and must seamlessly consume and be consumed by code not written with the same principles in mind.
  • Intuitive and Ergonomic: The patterns should be self-explanatory, allowing developers to grasp and implement them at a glance, minimizing the risk of misinterpretations that could result in anti-patterns or unexpected behaviors.
  • Exploit TypeScript: Leverage TypeScript's type system to provide immediate feedback through IDE features like syntax highlighting, error detection, and auto-completion.

Now, let's dive into the heart of the matter by addressing our first key challenge. Let's introduce the term task for functions that may either succeed or encounter an error.

function task() {
  if (condition) {
    throw new Error("failed");
  }

  return "value";
}

We need an error-handling approach that keeps control flow clean, keeps developers constantly aware of potential failures, and maintains type safety throughout. One idea worth exploring is the concept of returning errors instead of throwing them. Let's see how this might look:

function task() {
  if (condition) {
    // return instead of throwing.
    return new Error("failed");
  }

  return "value";
}

By introducing Errors as values and assigning them specific meaning, we enhance the expressivity of a task's return value, which can now represents either successful or failing outcomes. TypeScript’s type system becomes particularly effective here, typing the result as string | Error, and flagging any attempt to use the result without first checking for errors. This ensures safer code practices. Once error checks are performed, type narrowing allows us to work with the success value free from the Error type.

const result: string | Error = task();

// Handle the error.
if (result instanceof Error) {
  return;
}

result;
// ?^ result: string

Managing multiple errors becomes reliable with TypeScript’s type checker, which guides the process through autocompletion and catches mistakes at compile time, ensuring a type-driven and dependable workflow.

function task() {
  if (condition1) return new CustomError1();
  if (condition2) return new CustomError2();
  return "value";
}

// In another file...
const result = task();

if (result instanceof CustomError1) {
  // Handle CustomError1.
} else if (result instanceof CustomError2) {
  // Handle CustomError2.
}

And since we're just working within plain JavaScript, we can seamlessly integrate existing libraries to enhance our error handling. For example, the powerful ts-pattern library synergize beautifully with this approach:

import { match } from "ts-pattern";

match(result)
  .with(P.instanceOf(CustomError1), () => {
    /* Handle CustomError1 */
  })
  .with(P.instanceOf(CustomError2), () => {
    /* Handle CustomError2 */
  })
  .otherwise(() => {
    /* Handle success case */
  });

We now face 2 types of errors: those returned by tasks adopting our convention and those thrown. As established in our guiding principles, we can't assume every function will follow our convention. This assumption is not only necessary to make our pattern useful and usable, but it also reflects the reality of JavaScript code. Even without explicit throws, runtime errors like "cannot read properties of null" can still occur unexpectedly.

Within our convention, we can classify returned errors as "expected" — these are errors we can anticipate, handle, and recover from. On the other hand, thrown errors belong to the "unexpected" category — errors we can't predict or generally recover from. These are best addressed at the highest levels of our program, primarily for logging or general awareness. Similar distinctions are built into the syntax of some other languages. For example, in Rust:

// Recoverable error.
Err("Task failed")

// Unrecoverable error.
panic!("Fatal error")

For third-party APIs whose errors we want to handle, we can wrap them in our own functions that conform to our error handling convention. This approach also gives us the opportunity to add additional context or transform the error into a more meaningful representation for our specific use case. Let's take fetch as an example, to demonstrate also how this pattern seamlessly extends to asynchronous functions:

async function $fetch(input: string, init?: RequestInit) {
  try {
    // Make the request.
    const response = await fetch(input, init);
    // Return the response if it's OK, otherwise an error.
    return response.ok ? response : new ResponseError(response);
  } catch (error) {
    // ?^ DOMException | TypeError | SyntaxError.
    // Any cause from request abortion to a network error.
    return new RequestError(error);
  }
}

When fetch returns a response with a non-2XX status code, it's often considered an unexpected result from the client's perspective, as it falls outside the normal flow. We can wrap such responses in a custom exception type (ResponseError), while keeping other network or parsing issues in their own type (RequestError).

const response: Response | ResponseError | RequestError = await $fetch("/api");

This is an example of how we can wrap third-party APIs to enrich the expressiveness of their error handling. This approach also allows for progressive enhancement — whether you’re incrementally refactoring existing try/catch blocks or just starting to add proper error types in a codebase that’s never even heard of try/catch. (Yes, we know you’re out there.)

Another important aspect to consider is task composition, where we need to extract the results from multiple tasks, process them, and return a new value. In case any task returns an error, we simply stop the execution and propagate it back to the caller. This kind of task composition can look like this:

function task() {
  // Compute the result and exclude the error.
  const result1: number | Error1 = task1();
  if (result1 instanceof Error1) return result1;

  // Compute the result and exclude the error.
  const result2: number | Error2 = task2();
  if (result2 instanceof Error2) return result2;

  const result = result1 + result2;
}

The return type of the task is correctly inferred as number | Error1 | Error2, and type narrowing allow removing the Error types from the return values. It works, but it's not very concise. To address this issue, languages like Zig have a dedicated operator:

pub fn task() !void {
  const value = try mayFail();
  // ...
}

We can achieve something similar in TypeScript with a few simple tricks. Our goal is to create a more concise and readable way of handling errors while maintaining type safety. Let's attempt to define a similar utility function which we'll call $try, it could look something like this:

function task() {
  const result1: number = $try(task1());
  const result2: number = $try(task2());

  return result1 + result2;
}

This code looks definitely cleaner and more straightforward. Internally, the function could be implemented like this:

function $try<t>(result: T): Exclude<t error> {
  if (result instanceof Error) throw result;
  return result;
}
</t></t>

The $try function takes a result of type T, checks if it's an Error, and throws it if so. Otherwise, it returns the result, with TypeScript inferring the return type as Exclude.

We've gained a lot in readability and clarity, but we've lost the ability to type expected errors, moving them to the unexpected category. This isn't ideal for many scenarios.

We need a native way to collect the errors types, perform type narrowing, and terminate execution if an error occurs, but we are running short on JavaScript constructs. Fortunately, Generators can come to our rescue. Though often overlooked, they can effectively handle complex control flow problems.

With some clever coding, we can use the yield keyword to extract the return type from our tasks. yield passes control to another process that determines whether to terminate execution based on whether an error is present. We’ll refer to this functionality as $macro, as if it extends the language itself:

// ?^ result: number | Error1 | Error2
const result = $macro(function* ($try) {
  const result1: number = yield* $try(task1());
  const result2: number = yield* $try(task2());

  return result1 + result2;
});

We'll discuss the implementation details later. For now, we've achieved our compact syntax at the cost of introducing an utility. It accepts tasks following our convention and returns a result with the same convention: this ensures the abstraction remains confined to its intended scope, preventing it from leaking into other parts of the codebase — neither in the caller nor the callee.

As it's still possible to have the "vanilla" version with if statements, paying for slightly higher verbosity, we've struck a good balance between conciseness and keeping everything with no abstraction. Moreover, we've got a potential starting point to inspire new syntax or a new part of the standard library, but that's for another post and the ECMAScript committee will have to wait for now.

Wrapping Up

Our journey could end here: we've highlighted the limitations of current error management practices in JavaScript, introduced a convention that cleanly separates expected from unexpected errors, and tied everything together with strong type definitions.

As obvious as it may seems, the real strength of this approach lies in the fact that most JavaScript functions are just a particular case of this convention, that happens to return no expected error. This makes integrating with code written without this convention in mind as intuitive and seamless as possible.

One last enhancement we can introduce is simplifying the handling of unexpected errors, which up to now still requires the use of try/catch. The key is to clearly distinguish between the task result and unexpected errors. Taking inspiration from Go's error-handling pattern, we can achieve this using a utility like:

const [result, err] = $trycatch(task);

This utility adopts a Go-style tuple approach, where the first element is the task's result, and the second contains any unexpected error. Exactly one of these values will be present, while the other will be null.

But we can take it a step further. By leveraging TypeScript's type system, we can ensure that the task's return type remains unknown until the error is explicitly checked and handled. This prevents the accidental use of the result while an error is present:

const [result, err] = $trycatch(() => "succeed!");
// ?^ result: unknown
// ?^ err: Error | null

if (err !== null) {
  return;
}

result;
// ?^ result: string

Due to JavaScript's dynamic nature, any type of value can be thrown. To avoid falsy values that can create and subtle bugs when checking for the presence of an error, err will be an Error object that encapsulates the thrown values and expose them through Error.cause.

To complete out utility, we can extend it to handle asynchronous functions and promises, allowing the same pattern to be applied to asynchronous operations:

// Async functions.
const [result, err] = await $trycatch(async () => { ... });

// Or Promises.
const [result, err] = await $trycatch(new Promise(...));

That's enough for today. I hope you’ve enjoyed the journey and that this work inspires new innovations in the Javascript and Typescript ecosystem.

How to implement the code in the articles, you ask? Well, of course there's a library! Jokes aside, the code is straightforward, but the real value lies in the design and thought process behind it. The repository serves as a foundation for ongoing discussions and improvements. Feel free to contribute or share your thoughts!

See you next time — peace ✌️.

TypeScript: a new Frontier for Error Management ts-zen / trycatch

Robust and Type-Safe Errors Management Conventions with Typescript


TypeScript: a new Frontier for Error Management

Robust and Type-Safe Errors Management Conventions with Typescript

TypeScript: a new Frontier for Error Management TypeScript: a new Frontier for Error Management TypeScript: a new Frontier for Error Management TypeScript: a new Frontier for Error Management TypeScript: a new Frontier for Error Management TypeScript: a new Frontier for Error Management

TypeScript: a new Frontier for Error Management
TypeScript: a new Frontier for Error Management

Philosophy

還沒讀過博文嗎?您可以在這裡找到它,深入了解該項目背後的設計和推理。以下是幫助您入門的快速快照:

JavaScript 的錯誤管理設計 落後於 Rust、Zig 和 Go 等現代語言。語言設計很困難,大多數提交給 ECMAScript 或 TypeScript 委員會的提案要么被拒絕,要么經歷極其緩慢的迭代過程。

該領域的大多數函式庫和用戶態解決方案都引入了屬於紅/藍函數問題的抽象,需要完整的程式碼庫採用並導致技術鎖定。

這個專案的目標是突破 JavaScript 中錯誤處理的界限,優先考慮約定而不是抽象並充分利用本機構造的潛力。我們提供了一套最小的實用程式來增強開發人員體驗,希望能夠激發未來的語言改進和...


在 GitHub 上查看


以上是TypeScript:錯誤管理的新領域的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
JavaScript,C和瀏覽器之間的關係JavaScript,C和瀏覽器之間的關係May 01, 2025 am 12:06 AM

引言我知道你可能會覺得奇怪,JavaScript、C 和瀏覽器之間到底有什麼關係?它們之間看似毫無關聯,但實際上,它們在現代網絡開發中扮演著非常重要的角色。今天我們就來深入探討一下這三者之間的緊密聯繫。通過這篇文章,你將了解到JavaScript如何在瀏覽器中運行,C 在瀏覽器引擎中的作用,以及它們如何共同推動網頁的渲染和交互。 JavaScript與瀏覽器的關係我們都知道,JavaScript是前端開發的核心語言,它直接在瀏覽器中運行,讓網頁變得生動有趣。你是否曾經想過,為什麼JavaScr

node.js流帶打字稿node.js流帶打字稿Apr 30, 2025 am 08:22 AM

Node.js擅長於高效I/O,這在很大程度上要歸功於流。 流媒體匯總處理數據,避免內存過載 - 大型文件,網絡任務和實時應用程序的理想。將流與打字稿的類型安全結合起來創建POWE

Python vs. JavaScript:性能和效率注意事項Python vs. JavaScript:性能和效率注意事項Apr 30, 2025 am 12:08 AM

Python和JavaScript在性能和效率方面的差異主要體現在:1)Python作為解釋型語言,運行速度較慢,但開發效率高,適合快速原型開發;2)JavaScript在瀏覽器中受限於單線程,但在Node.js中可利用多線程和異步I/O提升性能,兩者在實際項目中各有優勢。

JavaScript的起源:探索其實施語言JavaScript的起源:探索其實施語言Apr 29, 2025 am 12:51 AM

JavaScript起源於1995年,由布蘭登·艾克創造,實現語言為C語言。 1.C語言為JavaScript提供了高性能和系統級編程能力。 2.JavaScript的內存管理和性能優化依賴於C語言。 3.C語言的跨平台特性幫助JavaScript在不同操作系統上高效運行。

幕後:什麼語言能力JavaScript?幕後:什麼語言能力JavaScript?Apr 28, 2025 am 12:01 AM

JavaScript在瀏覽器和Node.js環境中運行,依賴JavaScript引擎解析和執行代碼。 1)解析階段生成抽象語法樹(AST);2)編譯階段將AST轉換為字節碼或機器碼;3)執行階段執行編譯後的代碼。

Python和JavaScript的未來:趨勢和預測Python和JavaScript的未來:趨勢和預測Apr 27, 2025 am 12:21 AM

Python和JavaScript的未來趨勢包括:1.Python將鞏固在科學計算和AI領域的地位,2.JavaScript將推動Web技術發展,3.跨平台開發將成為熱門,4.性能優化將是重點。兩者都將繼續在各自領域擴展應用場景,並在性能上有更多突破。

Python vs. JavaScript:開發環境和工具Python vs. JavaScript:開發環境和工具Apr 26, 2025 am 12:09 AM

Python和JavaScript在開發環境上的選擇都很重要。 1)Python的開發環境包括PyCharm、JupyterNotebook和Anaconda,適合數據科學和快速原型開發。 2)JavaScript的開發環境包括Node.js、VSCode和Webpack,適用於前端和後端開發。根據項目需求選擇合適的工具可以提高開發效率和項目成功率。

JavaScript是用C編寫的嗎?檢查證據JavaScript是用C編寫的嗎?檢查證據Apr 25, 2025 am 12:15 AM

是的,JavaScript的引擎核心是用C語言編寫的。 1)C語言提供了高效性能和底層控制,適合JavaScript引擎的開發。 2)以V8引擎為例,其核心用C 編寫,結合了C的效率和麵向對象特性。 3)JavaScript引擎的工作原理包括解析、編譯和執行,C語言在這些過程中發揮關鍵作用。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具