我有一个简单的timeout
函数,它使用超时包装异步函数,以确保它在预设的时间后失败。超时功能如下:
export default async function<T = any>(
fn: Promise<T>,
ms: number,
identifier: string = ""
): Promise<T> {
let completed = false;
const timer = setTimeout(() => {
if (completed === false) {
const e = new Error(`Timed out after ${ms}ms [ ${identifier} ]`);
e.name = "TimeoutError";
throw e;
}
}, ms);
const results = await fn;
completed = true;
timer.unref();
return results;
}
然后,我在这个简单的代码片段中使用此函数,以确保将获取请求(使用node-fetch
实现)转换为文本输出:
let htmlContent: string;
try {
htmlContent = await timeout<string>(
response.text(),
3000,
`waiting for text conversion of network payload`
);
} catch (e) {
console.log(
chalk.grey(`- Network response couldn\'t be converted to text: ${e.message}`)
);
problemsCaching.push(business);
return business;
}
在多次迭代中运行此代码时,大多数URL端点都提供了一个可以轻松转换为文本的有效负载,但偶尔会出现一个似乎只挂起fetch调用的URL。在这种情况下,超时确实会触发,但抛出的TimeoutError
不会被catch块捕获,而是终止正在运行的程序。
我有点困惑。我现在使用异步/等待很多,但我的理解可能仍然有一些粗糙的边缘。任何人都可以解释我如何有效地捕获此错误并处理它?
抛出的错误只有在其直接封闭的函数具有某种错误处理时才会被捕获。传递给setTimeout
的匿名函数不是async
函数本身,所以如果单独的async
在一段时间后抛出,timeout
函数将不会停止执行:
const makeProm = () => new Promise(res => setTimeout(res, 200));
(async () => {
setTimeout(() => {
throw new Error();
}, 200);
await makeProm();
console.log('done');
})()
.catch((e) => {
console.log('caught');
});
这似乎是使用Promise.race
的好时机:传递给它fetch
Promise
,并且还传递了一个Promise
,在通过ms
参数后拒绝:
async function timeout(prom, ms) {
return Promise.race([
prom,
new Promise((res, rej) => setTimeout(() => rej('timeout!'), ms))
])
}
(async () => {
try {
await timeout(
new Promise(res => setTimeout(res, 2000)),
500
)
} catch(e) {
console.log('err ' + e);
}
})();
此错误发生在单独的调用堆栈中,因为它是从回调中抛出的。它完全独立于try
/ catch
块内的同步执行流程。
您希望在超时或成功回调中操作相同的promise对象。这样的事情应该更好:
return new Promise( ( resolve, reject ) => {
let rejected = false;
const timer = setTimeout( () => {
rejected = true;
reject( new Error( 'Timed out' ) );
}, ms ).unref();
fn.then( result => {
clearTimeout( timer );
if ( ! rejected ) {
resolve( result ) );
}
} );
} );
如果没有rejected
和clearTimeout
,它可能会工作得很好,但这样你确保调用resolve
或reject
,而不是两者。
你会注意到我没有在这里任何地方使用await
或throw
!如果您遇到异步代码问题,最好首先使用单一样式(所有回调,或所有承诺,或使用await
的所有“同步”样式)编写它。
特别是这个例子不能仅使用await
编写,因为你需要同时运行两个任务(超时和请求)。你可以使用Promise.race()
,但你仍然需要一个Promise
来使用。