Try Catch Finally in JS


javascript 中,try catch finally 是用来捕获错误的关键字组合。在实际使用时,它们的组合使用方式可能会影响代码的执行顺序。在这篇文章中,我们会讨论几种令人困惑的模式,以更好的理解这些关键字,避免不当使用。

return before finally

function foo() {
  try {
    return 'foo returns';
  } catch (e) {
    //  ignore
  } finally {
    console.log('foo completed');
  }
}

function bar() {
  console.log(foo());
  console.log('bar completed')
}

bar();

这段代码令人困惑的点在于 finally 代码块与 try 代码块的执行先后顺序的问题。按照通常的理解,try 代码块先执行,则函数已 return,调用栈已弹出,给到上层调用的下一语句,此时又怎么可能回到该函数内去执行其 finally 代码块呢?

实际运行该代码,结果如下:

foo completed
foo returns
bar completed

可以看到,finally 代码块先于 try 代码块中的 return 执行了。这说明 finally 有一个有趣的特点:它的确总是执行的,但是执行时机不是等待 try catch 代码块执行完再执行,而是在 try catch 代码块退出(return 也是对代码块的退出)之前总是执行。

既然有这个区别,那么以下一些令人困惑的代码的执行结果也就都可以令人理解了:

function foo() {
  try {
    return true;
  } finally {
    return false;
  }
}
foo(); // return `false` because finally returns first

function bar() {
  let i = 0;
  try {
    console.log('try', i);
    return ++i; //  just before this returns, the result value `1` is recorded(as a copy because it is a number rather than reference), and function goes into `finally` block to modify i again
  } finally {
    console.log('finally', i);
    ++i;
    console.log('finally', i);
  }
}
bar();  //  outputs `try 0` `finally 1` `finally 2` and returns 1

try finally without catch

function foo() {
  try {
    throw new Error('exception in try');
  } finally {
    throw new Error('exception in finally');
  }
}
function bar() {
  try {
    foo();
  } catch (e) {
    console.log('caught error in foo():', e.message);
  }
}
bar();

函数 foo() 没有 catch 代码块,当 try 代码块内发生错误时,错误会被抛出或者被上层的函数调用所捕获吗?直觉告诉我这里只是写法上省略了 catch,相当于有一个空的 catch 所以错误不会被抛出。执行结果如下:

caught error in foo(): exception in finally

可以看到, finally 语句中抛出的错误优先执行,符合上例的认知;没有写 catch 并不意味着错误可以被默认吞掉,而是正常的抛出,因此外层依然可以捕获到该错误。

catch while finally throws error

function foo() {
  try {
    return true;
  } catch (e) {
    console.log('caught error:', e);
  } finally {
    throw new Error('exception in finally');
  }
}
foo();

该函数使用了完整的 try catch finally,毫无疑问,catch 是能捕获 try 代码块中的异常的。但是如果在 finally 的代码里块发生了异常,按照上面所说的 finally 执行时机的问题,catch 能否捕获到呢?执行结果如下:

Uncaught Error: exception in finally
  at foo (<anonymous>:8:11)
  at <anonymous>:11:1

可以看到,catch 并没有能捕获到 finally 中的异常。原来,catch 只能捕获 try 代码块中的异常,即便 finally 的执行时机在 try catch 代码块退出之前,也无法捕获到。

finally 类似,catch 代码块中发生的错误也会被抛出到上层:

function foo() {
  try {
    throw new Error('a');
  } catch (e) {
    console.log('caught error:', e);
    throw new Error('b');
  }
}
foo();  //  outputs: 'caught error: Error: a', 'Uncaught Error: b'

try return await in async functions

在异步函数的场景下进行错误捕获,可能情况会更加复杂。例如:

function tick(ok) {
  return ok ? Promise.resolve('ok') : Promise.reject(new Error('fail'));
}

async function foo() {
  try {
    return tick(false);
  } catch (e) {
    console.log('foo caught error:', e);
  }
}

async function bar() {
  try {
    const r = await foo();
    console.log('bar result', r);
  } catch (e) {
    console.log('bar caught error:', e);
  }
}
bar();  //  bar caught error: Error: fail

以上代码中的异步函数 foo 返回了一个失败的 promise 对象给到另外一个异步函数 bar 的上下文并执行结果等待 await,那么被 barcatch 所捕获是理所当然的事情了。但是如果是以下这种场景:

function tick(ok) {
  return ok ? Promise.resolve('ok') : Promise.reject(new Error('fail'));
}

async function foo() {
  try {
    return await tick(false); //  add 'await'
  } catch (e) {
    console.log('foo caught error:', e);
  }
}

async function bar() {
  try {
    const r = await foo();
    console.log('bar result', r);
  } catch (e) {
    console.log('bar caught error:', e);
  }
}
bar();  //  foo caught error: Error: fail

那么异常将被 foo 中的 catch 所捕获到(并且被吞掉),bar 拿到的 foo() 的执行结果为 undefined. 由此我们可以发现,异步函数中的 return await PromiseObj 以及 return PromiseObj 并不能一概而论的做等价转化,在 try catch 时还是应该具体分析代码的执行顺序以决定如何使用。

references