这种情况有多常见?
function writeTransactionsToFile(transactions) {
let writeStatus;
try {
fs.writeFileSync('transactions.txt', transactions);
writeStatus = 'success';
} catch (error) {
writeStatus = 'error';
}
// do something with writeStatus...
}
这是另一个我们想要一个取决于是否存在异常的值的实例。
通常, 我们可能会在 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'
});
// do something with 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) {
// 返回 'success' 或 'error'
const writeStatus = tryCatch<'success' | 'error'>({
tryFn: () => {
fs.writeFileSync('transaction.txt', transactions);
return 'success';
},
catchFn: (error) => return 'error';
});
// do something with writeStatus...
}
我们使用 'success' | 'error' 联合类型来限制我们可以从 try 和 catch 回调中返回的字符串。
异步处理
不,我们完全不需要担心这个问题 - 如果 tryFn 或 catchFn 是 async ,那么 writeTransactionToFile() 会自动返回一个 Promise 。
这是我们大多数人应该熟悉的另一个 try-catch 情况:发出网络请求并处理错误。
在这里,我们根据请求是否成功来设置一个外部变量(在try-catch 之外)——在 React 应用中,我们可以轻松地用它设置状态。
显然,在真实世界的应用程序中,请求将会是异步的,以避免阻塞用户界面线程:
async function comment(comment: string) {
type Status = 'error' | 'success';
let commentStatus: Status;
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';
}
// do something with commentStatus...
}
我们再次需要在这里创建一个可变变量,以便它可以进入 try-catch 并且没有作用域错误地成功出来。
我们像以前一样进行重构,这次,我们 async 了 try 和 catch 函数,从而 await 了 tryCatch() :
async function comment(comment: string) {
type Status = 'error' | 'success';
// ⚠️ await because this returns Promise<Status>
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 }),
});
// ⚠️ functional conditional
return response.ok ? 'success' : 'error';
},
catchFn: async (error) => 'error',
});
// do something with commentStatus...
}
可读性,模块化,和单一职责
处理异常时遵循的两个 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));
附注:在命名标识符时,我建议我们尽可能地使用名词来表示变量,形容词来表示函数,而对于高阶函数……我们可以使用副词!就像一个故事,代码将更自然地阅读,并可能更好地理解。
所以,我们可以使用 silently ,而不是 tryCatch :
const getFileContents = (filePath) =>
silently(() => fs.readFileSync(filePath, 'utf8'));
const parseJSON = (content) =>
silently(() => JSON.parse(content));
总结
当然, try-catch 本身就能完美运行。
我们并没有丢弃它,而是将其转化为更易维护和可预测的工具。 tryCatch() 甚至只是许多使用像 try-catch 这样的命令式构造的声明式友好函数之一
如果更喜欢直接使用 try-catch ,请记住使用 2 个 try-catch 的经验法则,以提高您的代码的模块化和可读性。