在过去18年的编程生涯中,我几乎在每个项目中都要处理异步行为。
自从在JavaScript中采用async-await后,我们了解到async-await使很多代码更令人愉快,更容易推理。
最近我注意到,当我在处理一个需要异步连接和断开的资源时,我最终会写出这样的代码:
// NOT MY FAVORITE PATTERN
router.get('/users/:id', async (req, res) => {
const client = new Client();
let user;
try {
await client.connect();
user = await client.find('users').where('id', req.path.id);
} catch(error) {
res.status(500);
user = { error };
} finally {
await client.close();
}
res.json(user);
});
它变得冗长,因为我们必须使用try/catch来处理错误。
这种资源的例子包括数据库、ElasticSearch、命令行和ssh。 在这些用例中,我已经确定了一种代码模式,我称之为结果-错误模式。
考虑把上面的代码改写成这样:
// I LIKE THIS PATTERN BETTER
router.get('/users/:id', async (req, res) => {
const { result: user, error } = await withDbClient(client => {
return client.find('users').where('id', req.path.id);
});
if (error) {
res.status(500);
}
res.json({ user, error });
});
注意一些事情:
- 数据库客户端为我们创建,我们的回调可以直接利用它。
- 我们没有在try-catch块中捕获错误,而是依靠
withDbClient来返回错误。 - 结果总是被称为
result,因为我们的回调可以返回任何类型的数据。 - 我们不需要关闭资源。
那么,withDbClient 是做什么的呢?
- 它处理创建资源、连接和关闭。
- 它处理 try、catch 和 finally。
- 它确保从
withDbClient,不会有未捕获的异常被抛出。 - 它确保在处理程序中抛出的任何异常也会在
withDbClient中被捕获。 - 它确保
{ result, error }总是被返回。
下面是一个实现的例子:
// EXAMPLE IMPLEMENTATION
async function withDbClient(handler) {
const client = new DbClient();
let result = null;
let error = null;
try {
await client.connect();
result = await handler(client);
} catch (e) {
error = e;
} finally {
await client.close();
}
return { result, error };
}
更进一步
一个不需要被关闭的资源怎么办?好吧,结果错误模式仍然可以很好地解决这个问题。
考虑到以下对fetch 的使用:
// THIS IS NICE AND SHORT
const { data, error, response } = await fetchJson('/users/123');
它的实现可能是这样的:
// EXAMPLE IMPLEMENTATION
async function fetchJson(...args) {
let data = null;
let error = null;
let response = null;
try {
const response = await fetch(...args);
if (response.ok) {
try {
data = await response.json();
} catch (e) {
// not json
}
} else {
// note that statusText is always "" in HTTP2
error = `${response.status} ${response.statusText}`;
}
} catch(e) {
error = e;
}
return { data, error, response };
}
更高层次的使用
我们不必止步于低级别的使用。那其他可能以结果或错误结束的函数呢?
最近,我写了一个有大量ElasticSearch交互的应用程序。我决定在更高层次的函数上也使用结果-错误模式。
例如,搜索帖子会产生一个ElasticSearch文档的数组,并像这样返回结果和错误:
const { result, error, details } = await findPosts(query);
如果你使用过ElasticSearch,你会知道响应是冗长的,数据在响应中被嵌套了好几层。这里,result 是一个包含的对象:
records- 一个文档的数组total- 如果没有应用限制,文档的总数量aggregations- ElasticSearch的分面搜索信息
正如你可能猜到的,error 可能是一个错误信息,而details 是完整的 ElasticSearch 响应,以防你需要错误元数据、亮点或查询时间等东西。
我用一个查询对象搜索ElasticSearch的实现是这样的:
// Fetch from the given index name with the given query
async function query(index, query) {
// Our Result-Error Pattern at the low level
const { result, error } = await withEsClient(client => {
return client.search({
index,
body: query.getQuery(),
});
});
// Returning a similar object also with result-error
return {
result: formatRecords(result),
error,
details: result || error?.meta,
};
}
// Extract records from responses
function formatRecords(result) {
// Notice how deep ElasticSearch buries results?
if (result?.body?.hits?.hits) {
const records = [];
for (const hit of result.body.hits.hits) {
records.push(hit._source);
}
return {
records,
total: result.body.hits.total?.value || 0,
aggregations: result.aggregations,
};
} else {
return { records: [], total: null, aggregations: null };
}
}
然后,findPosts 函数就变成了这样一个简单的东西:
function findPosts(query) {
return query('posts', query);
}
总结
以下是实现结果-错误模式的函数的关键方面:
- 不要抛出异常。
- 总是返回一个带有结果和错误的对象,其中一个可能为空。
- 隐藏任何异步的资源创建或清理。
以下是调用实现结果-错误模式的函数的相应好处:
- 你不需要使用try-catch块。
- 处理错误情况就像
if (error)。 - 你不需要担心设置或清理的操作。
不要相信我的话,自己试试吧