功能型try-catch如何改变你的JavaScript代码

文章介绍了如何通过将传统的try-catch结构转换为功能型函数tryCatch(),提高代码的可读性和模块化。通过TypeScript和Promise处理异步操作,强调了遵循两个原则:try-catch应靠近错误源且每个函数只处理一个。文章还提倡使用副词命名法和静默处理异常,以增强代码的清晰度。
摘要由CSDN通过智能技术生成

这有多常见?

function writeTransactionsToFile(transactions) {
  let writeStatus;

  try {
    fs.writeFileSync('transactions.txt', transactions);
    writeStatus = 'success';
  } catch (error) {
    writeStatus = 'error';
  }
  // 做一些对writeStatus的操作...
}

这是另一个我们想要一个值的例子,这个值取决于是否有异常抛出。

通常,你很可能在try-catch外面创建一个可变变量,用于在try-catch内外无错误访问。

但不一定非要这么做。有了功能型try-catch就不一样了。

一个纯净的tryCatch()函数避免了可变变量,鼓励代码库中可维护性可预测性。没有外部状态被修改 - tryCatch()封装了整个错误处理逻辑并产生单一输出。

我们的catch变成了一个不需要大括号的单行语句。

function writeTransactionsToFile(transactions) {
  // 👇 我们现在可以使用const
  const writeStatus = tryCatch({
    tryFn: () => {
      fs.writeFileSync('transactions.txt', transactions);
      return 'success';
    },
    catchFn: (error) => 'error';
  });
  // 做一些对writeStatus的操作...  
}

tryCatch()函数

这个tryCatch()函数到底是什么样的呢?

从我们上面如何使用它,你已经可以猜到定义:

function tryCatch({ tryFn, catchFn }) {
  try {
    return tryFn(); 
  } catch (error) {
    return catchFn(error);
  }
}

为了恰当地讲述函数做什么,我们使用一个对象参数确保明确的参数名称,即使只有两个属性。因为编程不仅仅是达到目的的手段 - 我们也在通过代码讲述对象和数据的故事。

TypeScript在这里很有用,让我们看看一个泛型化tryCatch()可能是什么样子:

type TryCatchProps<T> = {
  tryFn: () => T;
  catchFn: (error: any) => T; 
};
function tryCatch<T>({ tryFn, catchFn }: TryCatchProps<T>): T {
  try {
    return tryFn();
  } catch (error) {
    return catchFn(error);
  }  
}

让我们测试一下,用TypeScript重写函数式writeTransactionsToFile():

function writeTransactionsToFile(transactions: string) {
  // 👇 返回'error' 或 'success'
  const writeStatus = tryCatch<'success' | 'error'>({
    tryFn: () => {
      fs.writeFileSync('transaction.txt', transactions);
      return 'success';
    },
    catchFn: (error) => return 'error';
  });

  // 做一些对writeStatus的操作...
}

我们使用 'success' | 'error'联合类型约束从trycatch回调中可以返回的字符串。

异步处理

不,我们不需要担心这个 - 如果 tryFncatchFnasync的,那么 writeTransactionToFile() 会自动返回一个Promise

下面是我们大多数人都应该熟悉的另一种 try-catch 情况:发出网络请求并处理错误。这里我们根据请求是否成功在 try-catch 外面设置一个外部变量 - 在 React 应用中我们可以用它轻松设置状态。

显然,在实际应用中请求会是异步的,以避免阻塞UI线程:

async function comment(comment: string) {
  type Status = 'error' | 'success';
  let commentStatus;
  try {
    const response = await fetch('https://api.mywebsite.com/comments', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ comment }),
    });

  if (!response.ok) {
      commentStatus = 'error';
    } else {
      commentStatus = 'success';
    }
  } catch (error) {
    commentStatus = 'error';
  }
  // 做一些对commentStatus的操作...
}

一次我们不得不在这里创建一个可变变量,这样它可以进入 try-catch 并在没有作用域错误的情况下胜利地走出来。

我们像之前一样重构,这次我们使 trycatch 函数为 async 方式,从而 await tryCatch():

async function comment(comment: string) {
  type Status = 'error' | 'success';
  // 👇 因为返回 Promise<Status> 所以 await
  const commentStatus = await tryCatch<Status>({
    tryFn: async () => {
      const response = await fetch<('https://api.mywebsite.com/comments', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ comment }),
      });
      // 👇 功能性条件
      return response.ok ? 'success' : 'error';
    },
    catchFn: async (error) => 'error';
  });
  // 做一些对commentStatus的操作...
}

使用新的 try-catch-fn NPM 包实现功能型 try-catch。

可读性、模块化和单一责任

处理异常时要遵循两个 try-catch 原则:

  1. try-catch应该尽可能接近错误来源,和
  2. 每个函数只使用一个 try-catch

它们会使你的代码在短期和长期内更易读易维护。

看看这里的 processJSONFile(),它遵循规则 1。第一个 try-catch 仅负责处理文件读取错误,没有其他事。try 不会再添加其他逻辑,所以 catch 也永远不会改变。

下一行的 try-catch 仅仅是这里处理 JSON 解析的。

function processJSONFile(filePath) {
    let contents;
    let jsonContents;

    // 第一个 try-catch 块来处理文件读取错误
    try {
        contents = fs.readFileSync(filePath, 'utf8');
    } catch (error) {
        // 记录错误 
        contents = null;
    }
    // 第二个 try-catch 块来处理 JSON 解析错误  
    try {
        jsonContents = JSON.parse(contents);
    } catch (error) {
       // 记录错误
       jsonContents = null;
    }
    return jsonContents;
}

但是 processJsonFile() 完全无视了规则 2,同一个函数里有两个 try-catch 块。

那么让我们通过重构把它们分到单独函数里去:

function processJSONFile(filePath) {
  const contents = getFileContents(filePath);

  const jsonContents = parseJSON(contents);
  return jsonContents; 
}

function getFileContents(filePath) {
  let contents;
  try {
    contents = fs.readFileSync(filePath, 'utf8');
  } catch (error) {
    contents = null;
  }
  return contents;
}

function parseJSON(content) {
  let json;
  try {
    json = JSON.parse(content);
  } catch (error) {
    json = null;
  }
  return json;  
}

但是我们现在有了 tryCatch(),它在这里非常合适:

function processJSONFile(filePath) {
  return parseJSON(getFileContents(filePath)); 
}

const getFileContents = (filePath) => 
  tryCatch({
    tryFn: () => fs.readFileSync(filePath, 'utf8'),
    catchFn: () => null,
  });
const parseJSON = (content) =>
  tryCatch({
    tryFn: () => JSON.parse(content),
    catchFn: () => null,
  });

我们正在静默处理异常 —— 这就是这些新函数的主要工作。

如果这经常发生,为什么不创建一个“静音器”版本,在成功时返回 try 函数的结果,在错误时返回空?

function tryCatch<T>(fn: () => T) {
  try {
    return fn();
  } catch (error) {
    return null; 
  }
}

进一步缩短代码:

function processJSONFile(filePath) {
  return parseJSON(getFileContents(filePath)); 
}

const getFileContents = (filePath) =>  
  tryCatch(() => fs.readFileSync(filePath, 'utf8'));
const parseJSON = (content) => tryCatch(() => JSON.parse(content));

附注: 在命名标识符时,我建议尽可能使用名词来命名变量,形容词命名函数,然后… 副词命名高阶函数! 像一个故事一样,代码会更自然地读起来,可能更容易理解。

所以,instead of tryCatch, 我们可以使用 silently:

const getFileContents = (filePath) =>  
  silently(() => fs.readFileSync(filePath, 'utf8'));

const parseJSON = (content) => silently(() => JSON.parse(content)); 

如果你使用过 @mui/stylesrecompose,你会发现他们的许多高阶函数使用副词短语命名 – withStyleswithStatewithProps等等,我相信这并非偶然。

最后思考

当然,try-catch本身就可以正常工作。

我们没有抛弃它,而是将其转化为一种更可维护、更可预测的工具。tryCatch() 甚至只是许多使用 try-catch 等命令式构造的声明式友好函数中的一个。

如果你更喜欢直接使用 try-catch,请记住遵循 2 个 try-catch 规则,以宝贵的模块化和可读性增强来打磨你的代码。

避免痛苦的bug,节省宝贵时间,阅读 每一个 JavaScript 做的疯狂事情,一个迷人的指南涵盖了 JavaScript 微妙的陷阱和鲜为人知的部分。

  • 26
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也想MK代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值