带有go/生锈错误的打字稿?没有尝试/捕捉?异端。
#javascript #typescript #go #rust

所以,让我们从关于我的一些背景故事开始。我是一位具有大约10年经验的软件开发人员,最初与PHP合作,然后逐渐过渡到JavaScript。另外,这是我有史以来的第一篇文章,所以请了解:)

我大约在5年前的某个地方开始使用Typescript,从那以后,我再也没有回到JavaScript。我开始使用它的那一刻,我认为这是有史以来最好的编程语言。每个人都喜欢它,每个人都使用它...这只是最好的,对吗?正确的?对吗?

是的,然后我开始与其他语言(更现代的语言)一起玩耍。首先是去,然后我慢慢将Rust添加到我的列表中(感谢Prime)。

当您不知道存在不同的事物时,很难错过事情。

我在说什么?什么常见的东西分享?错误。对我来说最突出的一件事。更具体地说,这些语言如何处理它们。

javaScript依靠投掷异常来处理错误,而生锈将它们视为值。您可能会认为这没什么大不了的……但是,男孩,这听起来像是一件琐碎的事情。但是,这是一个改变游戏规则的人。

让我们穿过它们。我们不会深入研究每种语言。我们只想知道一般方法。

让我们从javascript / tyspript和一个小游戏开始。< / p>

给自己5秒钟以查看下面的代码,并回答为什么我们需要将其包装在try / catch中。

try {
    const request = { name: "test", value: 2n };
    const body = JSON.stringify(request);
    const response = await fetch("https://example.com", {
        method: "POST",
        body,
    });
    if (!response.ok) {
        return;
    }
    // handle response
} catch (e) {
    // handle error
    return;
}

因此,我认为大多数人都猜测,即使我们正在检查response.ok,Fetch方法仍然可能会丢失错误。 response.ok仅“捕获” 4xx和5xx网络错误。但是当网络本身失败时,它会引发错误。

,但我想知道你们中有多少人猜测JSON.stringify也会犯错。原因是请求对象包含bigint (2n)变量,而JSON不知道如何串制。

所以第一个问题是,我个人认为这是有史以来最大的JavaScript问题:我们不知道会出现错误。从JavaScript错误的角度来看,它与:
相同

try {
    let data = "Hello";
} catch (err) {
    console.error(err);
}

JavaScript不知道,JavaScript不在乎;你应该知道。

第二件事,这是一个完美的可行代码:

const request = { name: "test", value: 2n };
const body = JSON.stringify(request);
const response = await fetch("https://example.com", {
    method: "POST",
    body,
});
if (!response.ok) {
    return;
}

没有错误,没有衬里,即使这可能会破坏您的应用。

现在在我的脑海中,我可以听到:“有什么问题,只需在任何地方尝试 /捕捉。”这是第三个问题;我们不知道哪一投。当然,我们可以以错误消息来以某种方式猜测,但是对于更大的服务 /功能,可能会发生错误吗?您确定您是否通过一次尝试 /捕获来正确处理所有这些?< / p>

好吧,是时候停止挑选JS并移至其他东西了。让我们从Go开始:

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

我们正在尝试打开一个文件,该文件正在返回文件或错误。您会看到很多东西,主要是因为我们知道哪些函数始终返回错误。你永远不会错过一个。这是将误差视为值的第一个示例。您指定哪些功能可以返回它们,返回它们,分配它们,检查它们,与它们一起工作。

它也不是那么色彩丰富,也是批评的事情之一,error-checking codeif err != nil { ....有时比其他代码更多。

if err != nil {
    ...
    if err != nil {
        ...
        if err != nil {
            ... 
        }
    }  
}
if err != nil {
    ... 
}
...
if err != nil {
    ... 
}

仍然值得付出努力,相信我。

最后生锈:

let greeting_file_result = File::open("hello.txt");

let greeting_file = match greeting_file_result {
    Ok(file) => file,
    Err(error) => panic!("Problem opening the file: {:?}", error),
};

在这里显示的三个中最详细,具有讽刺意味的是最好的。因此,首先,Rust使用其惊人的枚举来处理错误(它们与打字稿枚举不同!)。不详细介绍,这里重要的是它使用了一个称为Result的枚举,带有两个变体:OkErr。您可能会猜到,Ok具有一个值,Err保持...惊喜,错误:d。

它还有很多方法可以更方便地处理它们,以减轻GO问题。最著名的是?操作员。

let greeting_file_result = File::open("hello.txt")?;

这里的摘要是,始终在哪里出现错误。他们迫使您在(大部分)出现的地方对其进行处理。没有隐藏的,没有猜测,没有带有惊喜面的破坏应用。

这种方法更好。一英里。

好吧,说实话,我有点撒谎了。我们不能像Go / Rust遇到的那样使打字稿错误奏效。这里的限制因素是语言本身。它只是没有适当的工具来做到这一点。

但是我们能做的就是尝试使其相似。并使其简单。

从此开始:

export type Safe<T> =
    | {
          success: true;
          data: T;
      }
    | {
          success: false;
          error: string;
      };

这里没有什么真正喜欢的,只是一种简单的通用类型。但是这个小婴儿可以完全更改代码。您可能会注意到,这里最大的区别是我们要么返回数据或错误。听起来很熟悉?

另外,第二个谎言,我们需要一些尝试 /捕捉。好事是我们只需要至少两个,而不是100000。

export function safe<T>(promise: Promise<T>, err?: string): Promise<Safe<T>>;
export function safe<T>(func: () => T, err?: string): Safe<T>;
export function safe<T>(
    promiseOrFunc: Promise<T> | (() => T),
    err?: string,
): Promise<Safe<T>> | Safe<T> {
    if (promiseOrFunc instanceof Promise) {
        return safeAsync(promiseOrFunc, err);
    }
    return safeSync(promiseOrFunc, err);
}

async function safeAsync<T>(
    promise: Promise<T>, 
    err?: string
): Promise<Safe<T>> {
    try {
        const data = await promise;
        return { data, success: true };
    } catch (e) {
        console.error(e);
        if (err !== undefined) {
            return { success: false, error: err };
        }
        if (e instanceof Error) {
            return { success: false, error: e.message };
        }
        return { success: false, error: "Something went wrong" };
    }
}

function safeSync<T>(
    func: () => T, 
    err?: string
): Safe<T> {
    try {
        const data = func();
        return { data, success: true };
    } catch (e) {
        console.error(e);
        if (err !== undefined) {
            return { success: false, error: err };
        }
        if (e instanceof Error) {
            return { success: false, error: e.message };
        }
        return { success: false, error: "Something went wrong" };
    }
}

“哇,真是个天才,他为尝试 /捕捉创造了一个包装纸。”是的你是对的;这只是我们的Safe类型的包装器作为返回。但是有时候您需要简单的事情。让我们将它们与上面的示例结合在一起。

旧一(16行):

try {
    const request = { name: "test", value: 2n };
    const body = JSON.stringify(request);
    const response = await fetch("https://example.com", {
        method: "POST",
        body,
    });
    if (!response.ok) {
        // handle network error
        return;
    }
    // handle response
} catch (e) {
    // handle error
    return;
}

新的(20行):

const request = { name: "test", value: 2n };
const body = safe(
    () => JSON.stringify(request),
    "Failed to serialize request",
);
if (!body.success) {
    // handle error (body.error)
    return;
}
const response = await safe(
    fetch("https://example.com", {
        method: "POST",
        body: body.data,
    }),
);
if (!response.success) {
    // handle error (response.error)
    return;
}
if (!response.data.ok) {
    // handle network error
    return;
}
// handle response (body.data)

是的,我们的新解决方案更长,但是:

  • 没有试用catch
  • 我们处理发生的每个错误
  • 我们可以为特定功能指定错误消息
  • 我们有一个不错的自上而下逻辑,顶部所有错误,然后只有底部的响应

,但现在来了。如果我们忘了检查这个:
会发生什么:

if (!body.success) {
    // handle error (body.error)
    return;
}

事情是...我们不能。是的,我们必须进行检查;如果我们不这样做,那么body.data将不存在。 LSP将通过在类型'Safe'上扔“属性'数据”来提醒我们。这全都归功于我们创建的简单的Safe类型。而且它也适用于错误消息;在检查!body.success之前,我们无法访问body.error

这是我们真正应该欣赏打字稿及其如何改变JavaScript世界的时刻。

也是如此:

if (!response.success) {
    // handle error (response.error)
    return;
}

我们无法删除!response.success检查,因为否则response.data将不存在。

当然,我们的解决方案并非没有问题,最大的解决方案是您需要记住要包装诺言 /功能,这些诺言 /功能可能会给我们的safe包装提供错误。这个“我们需要知道”是我们无法克服的语言限制。

听起来可能很难,但事实并非如此。您很快就开始意识到,您在代码中拥有的几乎所有承诺都会丢弃错误,并且可以知道它们的同步功能,并且没有太多的函数。

,您可能会问,值得吗?我们认为是,并且在我们的团队中正常工作:)。当您查看较大的服务文件时,在任何地方都没有尝试 /捕捉,每个错误都在出现的位置处理,逻辑流很好...它看起来不错。< / p>

这是现实生活中的用法(Sveltekit表格):

export const actions = {
    createEmail: async ({ locals, request }) => {
        const end = perf("CreateEmail");

        const form = await safe(request.formData());
        if (!form.success) {
            return fail(400, { error: form.error });
        }
        const schema = z
            .object({
                emailTo: z.string().email(),
                emailName: z.string().min(1),
                emailSubject: z.string().min(1),
                emailHtml: z.string().min(1),
            })
            .safeParse({
                emailTo: form.data.get("emailTo"),
                emailName: form.data.get("emailName"),
                emailSubject: form.data.get("emailSubject"),
                emailHtml: form.data.get("emailHtml"),
            });
        if (!schema.success) {
            console.error(schema.error.flatten());
            return fail(400, { form: schema.error.flatten().fieldErrors });
        }

        const metadata = createMetadata(URI_GRPC, locals.user.key)
        if (!metadata.success) {
            return fail(400, { error: metadata.error });
        }
        const response = await new Promise<Safe<Email__Output>>((res) => {
            usersClient.createEmail(schema.data, metadata.data, grpcSafe(res));
        });
        if (!response.success) {
            return fail(400, { error: response.error });
        }
        end();
        return {
            email: response.data,
        };
    },
} satisfies Actions;

有几件事要指出:

  • 我们的自定义函数grpcSafe帮助我们使用GRPC回调
  • createMetadata返回Safe内部,所以我们不需要包装它。
  • zod库使用相同的模式:)如果我们不进行schema.success检查,我们将无法访问schema.data

看起来不干净吗?所以尝试一下!也许也很适合您:)

另外,我希望这篇文章对您很有趣。我希望创建更多的人分享我的想法和想法。

P.S。看起来相似吗?

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f
const response = await safe(fetch("https://example.com"));
if (!response.success) {
    console.error(response.error);
    return;
}
// do something with the response.data