这有多常见?
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'
联合类型约束从try
和catch
回调中可以返回的字符串。
异步处理
不,我们不需要担心这个 - 如果 tryFn
或 catchFn
是async
的,那么 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
并在没有作用域错误的情况下胜利地走出来。
我们像之前一样重构,这次我们使 try
和 catch
函数为 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
原则:
try-catch
应该尽可能接近错误来源,和- 每个函数只使用一个
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/styles
或 recompose
,你会发现他们的许多高阶函数使用副词短语命名 – withStyles
、withState
、withProps
等等,我相信这并非偶然。
最后思考
当然,try-catch
本身就可以正常工作。
我们没有抛弃它,而是将其转化为一种更可维护、更可预测的工具。tryCatch()
甚至只是许多使用 try-catch
等命令式构造的声明式友好函数中的一个。
如果你更喜欢直接使用 try-catch
,请记住遵循 2 个 try-catch
规则,以宝贵的模块化和可读性增强来打磨你的代码。
避免痛苦的bug,节省宝贵时间,阅读 每一个 JavaScript 做的疯狂事情,一个迷人的指南涵盖了 JavaScript 微妙的陷阱和鲜为人知的部分。