在工作中,有人问在JavaScript中使用异步/等待语法时,是否有更好的方法来处理错误。他们不喜欢自己漂亮、简短、可读的代码行突然被尝试/捕获所包裹。在过去的几年里,我也对各种各样的在线热情感到沮丧,这些热情围绕着异步/等待,只是为了展示完全忽略错误处理的代码示例。
下面是使用异步/等待处理错误的一种更简单的方法,即返回函数式编程中已知的任一值。我的不像FP社区的“左右”那么正式它只是一个简单的JavaScript对象,在某种程度上遵循了节点回调命名约定。
第一个选项是创建承诺,只使用非此即彼的方式来成功。使用非此即彼的方式解决问题。第二种选择是使用简单的包装函数。
我们试图解决的问题是获取这些可读性最强的代码:
const go = async () => {
const data = await readFile('some.json');
const json = await parseJSON(data);
};
...意识到你忘了试捕。这种情况下的“读操作”是:
const readFile = () => Promise.reject(new Error('b00mz'));
哦不!让我们保护我们的功能来处理:
const go = async () => {
try {
const data = await readFile('some.json');
try {
const json = await parseJSON(data);
} catch (parseJSONError) {
console.error(`parseJSON failed: ${parseJSONError}`);
}
} catch (error) {
console.error(`readFile failed: ${error}`);
}
};
恶心。为了让她再次光彩照人,让我们先回到同步代码。
在JavaScript中使用异步/等待来确保不需要try/catch的最简单的方法是创建一个从不抛出错误的函数。相反,它会返回是否成功以及结果或错误。这就是Go的工作方式,Lua通过pcall。
这里有一个读取JSON文本文件的函数:
const readJSON = fileName => fs.readFileSync(filename).toString('utf8');
请注意,尽管这是一个简单的函数,但仍有许多事情可能出错:
其中两个会抛出一个错误,其中一个我们需要用try/catch手动包装。
相反,让我们像在围棋或夏威夷那样写它。我们将调用函数并检查响应,而不是调用函数并祈祷。这里的“响应”一词不同于“结果”像承诺一样,它是潜在好结果或坏结果的容器。
const parseJSON = o => {
try {
const data = JSON.parse(o);
return {
ok: true,
data
};
} catch (error) {
return {
ok: false,
error
};
}
};
const readJSON = o => {
try {
const string = fs.readFileSync(o).toString('utf8');
const result = parseJSON(string);
if (result.ok) {
return {
ok: true,
data: result.data
};
} else {
return result;
}
} catch (error) {
return {
ok: false,
error
}
}
};
现在,每个函数都尝试操作,如果成功,则报告操作成功以及数据。如果出现了故障,它会报告出了什么问题,同时报告错误。你像这样使用它:
const {
ok,
error,
data
} = readJSON('some.json');
if (ok) {
// use our data
} else {
console.error(`readJSON failed: ${error}`);
}
节点回调以类似的方式工作。如果异步调用不起作用,回调中的第一个参数就会出错。如果它确实起作用,该错误将是未定义的,并且您的数据将是第二个向下的参数:
fs.readFile('some.json', (error, data) => {
if (err) {
console.error(`readFile failed: ${error}`);
return;
}
// use our data
});
要带走的要点:
确保你永远不需要尝试/捕捉异步/等待的最简单方法是永远不要拒绝你的承诺。他们总是用上面的方法称成功为:
const readFile = filename =>
new Promise(success => {
try {
const data = fs.readFileSync(filename).toString('utf8');
success({
ok: true,
data
});
} catch (error) {
success({
ok: false,
error
})
}
});
这有一个好处,那就是保证承诺中的一系列承诺。所有人永远不会拒绝承诺,剩下的人的状态未知。
可悲的是,这太理想化了。你最有可能使用的是各种各样的没有承诺的库,或者它可能是你自己的代码,而且有很多。
相反,你可以用我的同事教我的东西,用一种叫做“确定的东西。”它是承诺的包装,以确保它总是成功的。
const sureThing = promise =>
promise
.then(data => ({
ok: true,
data
}))
.catch(error => Promise.resolve({
ok: false,
error
}))
秘密的调味汁是一个坚定的承诺的回报。这确保了承诺永远不会触发。在异步/等待中使用时捕获或抛出。
因此,假设我们的readFile和parseJSON是承诺:
const readFile = filename =>
new Promise((success, failure) =>
fs.readFile(filename, (error, data) =>
error ? success(data) :
failure(error)));
const parseJSON = o =>
new Promise((success, failure) => {
try {
const result = JSON.parse(o);
success(result);
} catch (error) {
failure(error);
}
});
使用sureItem,我们上面的例子现在可以重写了:
const go = async () => {
const readFileResult = await sureThing(readFile('some.json'));
if (readFileResult.ok) {
const {
ok,
error,
data
} = await sureThing(parseJSON(data));
if (ok) {
// use our data
} else {
return {
ok,
error
};
}
} else {
return readFileResult;
}
};
看起来非常必要,让开发人员可以控制如何处理这些错误,而不需要尝试/捕捉。
我希望您可以看到,通过创建不抛出错误的函数,您可以保持异步/等待代码try/catch空闲。通过使用一个简单的包装函数,您可以针对第三方代码或其他不遵循此规则的承诺来使用它。
现在,你们中的一些人可能会在看到这个结论后对我的术语“华丽”提出异议。是的,没有尝试/捕获,但是开发人员仍然被迫处理他们的控制流中的错误,使它成为一个if/then语句的嵌套。没有错误可以挖掘和寻找原因,很酷,但是...啊。我同意。在未来,我们将讨论如何更好地组合这些函数,使其更具可读性。你可以在readJSON
组成的同步示例中的函数readFile
和parseJSON
变成它自己。目前,这将使C#移植和Go/Lua爱好者重回正轨。