使用异步/等待更容易处理错误


在工作中,有人问在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');

请注意,尽管这是一个简单的函数,但仍有许多事情可能出错:

  • 文件可能不存在或由于某种原因无法读取。
  • 文件的编码不是UTF8,我们得到垃圾回收。
  • 由于上述任何错误,JSON.parse失败。

其中两个会抛出一个错误,其中一个我们需要用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
});

要点

要带走的要点:

  • 不要让你的函数抛出错误。
  • 返回一个带有确定布尔值的对象:如果您的函数工作正常并且有数据,则为true如果没有工作并且有错误,则为false。
  • 数据属性是您的数据;只有ok是真的,它才存在。
  • 错误属性是您的错误对象;只有ok是假的,它才存在。

永远兑现承诺

确保你永远不需要尝试/捕捉异步/等待的最简单方法是永远不要拒绝你的承诺。他们总是用上面的方法称成功为:

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组成的同步示例中的函数readFileparseJSON变成它自己。目前,这将使C#移植和Go/Lua爱好者重回正轨。