You're working on your TypeScript project. The code is clean and well-architected, you're proud of it. One day, an error pops up. Its stack trace stretches longer than the average npm install, bubbling up through countless layers that failed to handle it. Your code doesn't work, you have no idea where to start fixing it, every attempt feels like a clumsy patch. Your architecture doesn't look so clean anymore. You hate your project. You close your PC and go to enjoy your Friday.
The Error Management Wasteland
JavaScript's error management falls short of the expressive power and developer experience offered by modern languages like Rust, Zig, and Go. Its dynamic nature and lack of guardrails often leave developers navigating uncertainty, without the solid foundations and guarantees that stricter platforms provide.
This crucial pillar of software engineering is poorly reflected in the language's culture and ecosystem, with some of the most popular npm libraries failing to even mention exceptions in their documentation.
This lack of standards fosters in developers the misconception that exceptions rarely occur. As a result, this skewed perspective leads to a lack of interest in establishing such standards within the community.
Try-Catch: the Costs of Implicitness
The JavaScript try-catch model hides non-obvious implications. Exceptions can occur anywhere, yet anticipating them is surprisingly challenging. This seemingly simple pattern often obscures subtle pitfalls in everyday code:
let value; try { value = mayThrow(); } catch (e) { // Handle the exception. }
The first issue that stands out in the snippet is the scope expansion, with variables needing to be declared outside the try-catch block to maintain a contiguous control flow. This leads to more verbose, harder-to-track code, potentially introducing subtle bugs as the codebase grows in complexity.
The implicit nature of this dynamic error handling increases the cognitive load on the developers, requiring them to mentally track exception sources throughout the codebase. In contrast, explicit error handling models, like for instance the one in Go, compel developers to acknowledge and handle any error.
result, err := mayFail();
This is a huge win in the long term, facilitating smoother and safer maintenance as projects evolve.
Adding to these challenges, TypeScript's catch clause falls short in its ability to track and strictly type the errors that can be thrown, resulting in a loss of type safety at precisely the point where it's most crucial. JavaScript even allows throwing non-Error values, leaving us with practically no safeguards. Languages like Rust showcase the power and elegance of this approach with its error handling design:
match may_fail() { Ok(result) => println!("Success"), Err(Error::NotFound) => println!("Not found"), Err(Error::PermissionDenied) => println!("Permission denied"), }
Various proposals have been submitted to the TypeScript team, aiming to establish a foundation for a more robust and predictable exception system. However, these proposals have often been blocked by limitations in the underlying JavaScript platform, which lacks the necessary primitives to support such architectural enhancements.
Meanwhile, some proposals to address these shortcomings have also been submitted to TC39 (the committee for ECMAScript standardization), but they remain in the early stages of consideration. As Matt Pocock pointed out, the heat death of the universe is also making steady progress.
<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>
Seeking Community Solutions
When a language creates friction for innovation, the developer community often responds with ingenious libraries and user-land solutions. Many of the current proposals in this domain, like the exceptional Neverthrow, draw inspiration from functional programming, offering a suite of abstractions and utilities similar to Rust's Result type to address the problem:
function mayFail(): Result<string> { if (condition) { return err("failed"); } return ok("value"); } </string>
Another approach that stands out is the one of Effect. This powerful toolkit not only tackles error management head-on but also provides a comprehensive suite of utilities for handling asynchronous operations, resource management, and more:
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 ✌️.
ts-zen
/
trycatch
Robust and Type-Safe Errors Management Conventions with Typescript
Robust and Type-Safe Errors Management Conventions with Typescript
Philosophy
ブログ投稿をまだ読んでいませんか?このプロジェクトの背後にある設計と推論について詳しく知るには、ここで見つけることができます。開始するための簡単なスナップショットを次に示します:
JavaScript の エラー管理設計 は、Rust、Zig、Go などの現代言語に比べて遅れています。言語設計は難しく、ECMAScript 委員会や TypeScript 委員会へのほとんどの提案は拒否されるか、非常に遅い反復プロセスを経ることになります。
この分野のほとんどのライブラリとユーザーランド ソリューションでは、赤/青関数問題に該当する抽象化が導入されており、コードベースの完全な採用が必要となり、テクノロジのロックインが発生します。
このプロジェクトの目標は、JavaScript でのエラー処理の限界を押し広げ、抽象化よりも規約を優先し、ネイティブ構造を最大限に活用することです。私たちは、将来の言語の改善と…
위 내용은 TypeScript: 오류 관리를 위한 새로운 개척지의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

JavaScript 프레임 워크의 힘은 개발 단순화, 사용자 경험 및 응용 프로그램 성능을 향상시키는 데 있습니다. 프레임 워크를 선택할 때 : 1. 프로젝트 규모와 복잡성, 2. 팀 경험, 3. 생태계 및 커뮤니티 지원.

서론 나는 당신이 이상하다는 것을 알고 있습니다. JavaScript, C 및 Browser는 정확히 무엇을해야합니까? 그들은 관련이없는 것처럼 보이지만 실제로는 현대 웹 개발에서 매우 중요한 역할을합니다. 오늘 우리는이 세 가지 사이의 밀접한 관계에 대해 논의 할 것입니다. 이 기사를 통해 브라우저에서 JavaScript가 어떻게 실행되는지, 브라우저 엔진의 C 역할 및 웹 페이지의 렌더링 및 상호 작용을 유도하기 위해 함께 작동하는 방법을 알게됩니다. 우리는 모두 JavaScript와 브라우저의 관계를 알고 있습니다. JavaScript는 프론트 엔드 개발의 핵심 언어입니다. 브라우저에서 직접 실행되므로 웹 페이지를 생생하고 흥미롭게 만듭니다. 왜 Javascr

Node.js는 크림 덕분에 효율적인 I/O에서 탁월합니다. 스트림은 메모리 오버로드를 피하고 큰 파일, 네트워크 작업 및 실시간 애플리케이션을위한 메모리 과부하를 피하기 위해 데이터를 점차적으로 처리합니다. 스트림을 TypeScript의 유형 안전과 결합하면 Powe가 생성됩니다

파이썬과 자바 스크립트 간의 성능과 효율성의 차이는 주로 다음과 같이 반영됩니다. 1) 해석 된 언어로서, 파이썬은 느리게 실행되지만 개발 효율이 높고 빠른 프로토 타입 개발에 적합합니다. 2) JavaScript는 브라우저의 단일 스레드로 제한되지만 멀티 스레딩 및 비동기 I/O는 Node.js의 성능을 향상시키는 데 사용될 수 있으며 실제 프로젝트에서는 이점이 있습니다.

JavaScript는 1995 년에 시작하여 Brandon Ike에 의해 만들어졌으며 언어를 C로 실현했습니다. 1.C Language는 JavaScript의 고성능 및 시스템 수준 프로그래밍 기능을 제공합니다. 2. JavaScript의 메모리 관리 및 성능 최적화는 C 언어에 의존합니다. 3. C 언어의 크로스 플랫폼 기능은 자바 스크립트가 다른 운영 체제에서 효율적으로 실행하는 데 도움이됩니다.

JavaScript는 브라우저 및 Node.js 환경에서 실행되며 JavaScript 엔진을 사용하여 코드를 구문 분석하고 실행합니다. 1) 구문 분석 단계에서 초록 구문 트리 (AST)를 생성합니다. 2) 컴파일 단계에서 AST를 바이트 코드 또는 기계 코드로 변환합니다. 3) 실행 단계에서 컴파일 된 코드를 실행하십시오.

Python 및 JavaScript의 미래 추세에는 다음이 포함됩니다. 1. Python은 과학 컴퓨팅 분야에서의 위치를 통합하고 AI, 2. JavaScript는 웹 기술의 개발을 촉진하고, 3. 교차 플랫폼 개발이 핫한 주제가되고 4. 성능 최적화가 중점을 둘 것입니다. 둘 다 해당 분야에서 응용 프로그램 시나리오를 계속 확장하고 성능이 더 많은 혁신을 일으킬 것입니다.

개발 환경에서 Python과 JavaScript의 선택이 모두 중요합니다. 1) Python의 개발 환경에는 Pycharm, Jupyternotebook 및 Anaconda가 포함되어 있으며 데이터 과학 및 빠른 프로토 타이핑에 적합합니다. 2) JavaScript의 개발 환경에는 Node.js, VScode 및 Webpack이 포함되어 있으며 프론트 엔드 및 백엔드 개발에 적합합니다. 프로젝트 요구에 따라 올바른 도구를 선택하면 개발 효율성과 프로젝트 성공률이 향상 될 수 있습니다.


핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

VSCode Windows 64비트 다운로드
Microsoft에서 출시한 강력한 무료 IDE 편집기

드림위버 CS6
시각적 웹 개발 도구

Dreamweaver Mac版
시각적 웹 개발 도구

SublimeText3 Linux 새 버전
SublimeText3 Linux 최신 버전
