diff --git a/README.md b/README.md index 0ef515aa..386f13db 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ Then configure the rules you want to use under the rules section. "promise/avoid-new": "warn", "promise/no-new-statics": "error", "promise/no-return-in-finally": "warn", - "promise/valid-params": "warn" + "promise/valid-params": "warn", + "promise/wrap-await-with-try-catch": "warn" } } ``` @@ -92,6 +93,7 @@ or start with the recommended rule set: | [`valid-params`][valid-params] | Ensures the proper number of arguments are passed to Promise functions | :warning: | | | [`prefer-await-to-then`][prefer-await-to-then] | Prefer `await` to `then()` for reading Promise values | :seven: | | | [`prefer-await-to-callbacks`][prefer-await-to-callbacks] | Prefer async/await to the callback pattern | :seven: | | +| [`wrap-await-with-try-catch`][wrap-await-with-try-catch] | Ensures `await` expressions are wrapped with a try/catch | :warning: | | **Key** @@ -126,6 +128,7 @@ or start with the recommended rule set: [valid-params]: docs/rules/valid-params.md [prefer-await-to-then]: docs/rules/prefer-await-to-then.md [prefer-await-to-callbacks]: docs/rules/prefer-await-to-callbacks.md +[wrap-await-with-try-catch]: docs/rules/wrap-await-with-try-catch.md [nodeify]: https://www.npmjs.com/package/nodeify [pify]: https://www.npmjs.com/package/pify [@macklinu]: https://github.com/macklinu diff --git a/__tests__/wrap-await-with-try-catch.js b/__tests__/wrap-await-with-try-catch.js new file mode 100644 index 00000000..ba8938a8 --- /dev/null +++ b/__tests__/wrap-await-with-try-catch.js @@ -0,0 +1,106 @@ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../rules/wrap-await-with-try-catch') +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 8 + } +}) + +const message = '"await"s must be wrapped with a try/catch statement.' + +ruleTester.run('wrap-await-with-try-catch', rule, { + valid: [ + `async function test() { + try { + await doSomething() + } + catch(ex){ + errorHandler(ex) + } + }`, + `async function test() { + try { + throw Error + } + catch(ex0){ + try { + await doSomething() + } + catch(ex1) { + errorHandler(ex1) + } + } + }`, + `async function test() { + try { + (async function innerFn() { + try { + await doSomething() + } + catch (ex) { + errorHandler(ex) + } + })() + } + catch (ex) { + errorHandler(ex) + } + }`, + `var foo = async () => { + try { + await fetch(); + } catch (error) { + errorHandler(error); + } + }` + ], + invalid: [ + { + code: 'async function hi() { await doSomething() }', + errors: [{ message }] + }, + { + code: `async function test() { + try { + await doSomething() + } + finally { + errorHandler("ok.") + } + }`, + errors: [{ message }] + }, + { + code: `async function test() { + try { + throw Error + } + catch (ex) { + await doSomethingOther() + } + }`, + errors: [{ message }] + }, + { + code: `async function test() { + try { + (async function innerFn() { + await doSomething() + })() + } + catch (ex) { + errorHandler(ex) + } + }`, + errors: [{ message }] + }, + { + code: `var foo = async () => { + await fetch(); + }`, + errors: [{ message }] + } + ] +}) diff --git a/docs/rules/wrap-await-with-try-catch.md b/docs/rules/wrap-await-with-try-catch.md new file mode 100644 index 00000000..f95364aa --- /dev/null +++ b/docs/rules/wrap-await-with-try-catch.md @@ -0,0 +1,76 @@ +# Wrap awaits with try/catch blocks (wrap-await-with-try-catch) + +## Rule Details + +This rule is aimed at flagging awaits where they are not checked for possible rejections. + +Examples for **incorrect** code for this rule: + +```js +await doSomething() +``` + +```js +try { + ... +} +catch (ex) { + await doSomething(); +} +``` + +```js +try { + (async function someFn(){ + await doSomething(); + })(); +} +catch (ex) { + //... +} +``` + +Examples for **correct** code for this rule: + +```js +try { + await doSomething(); +} +catch (ex) { + //... +} +``` + +```js +try { + //... +} +catch (ex0) { + try { + await doSomething(); + } + catch (ex1) { + //... + } +} +``` + +```js +try { + (async function someFn(){ + try { + await doSomething(); + } + catch (exInner) { + //... + } + })(); +} +catch (ex) { + //... +} +``` + +## When Not To Use It + +If you handle awaits with a different error checking mechanism, you can disable this rule. diff --git a/index.js b/index.js index 6be65b45..d2674fb3 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,8 @@ module.exports = { 'avoid-new': require('./rules/avoid-new'), 'no-new-statics': require('./rules/no-new-statics'), 'no-return-in-finally': require('./rules/no-return-in-finally'), - 'valid-params': require('./rules/valid-params') + 'valid-params': require('./rules/valid-params'), + 'wrap-await-with-try-catch': require('./rules/wrap-await-with-try-catch') }, rulesConfig: { 'param-names': 1, @@ -39,7 +40,8 @@ module.exports = { 'promise/avoid-new': 'off', 'promise/no-new-statics': 'error', 'promise/no-return-in-finally': 'warn', - 'promise/valid-params': 'warn' + 'promise/valid-params': 'warn', + 'promise/wrap-await-with-try-catch': 'warn' } } } diff --git a/rules/wrap-await-with-try-catch.js b/rules/wrap-await-with-try-catch.js new file mode 100644 index 00000000..dd672c16 --- /dev/null +++ b/rules/wrap-await-with-try-catch.js @@ -0,0 +1,91 @@ +/** + * Rule: wrap-await-with-try-catch + */ + +'use strict' + +const getDocsUrl = require('./lib/get-docs-url') + +module.exports = { + meta: { + type: 'suggestion', + docs: { + url: getDocsUrl('wrap-await-with-try-catch') + } + }, + create(context) { + function isAwaitHandled() { + const ancestors = context.getAncestors() + let handledInOrder = [] + + ancestors.forEach(ancestor => { + if (ancestor.type === 'TryStatement') { + handledInOrder.push({ + name: 'try', + node: ancestor, + relatedTry: ancestor + }) + } else if (ancestor.type === 'CatchClause') { + handledInOrder.push({ + name: 'catch', + node: ancestor, + relatedTry: ancestor.parent + }) + } else if ( + ancestor.type === 'BlockStatement' && + ancestor.parent && + ancestor.parent.type === 'TryStatement' && + ancestor.parent.finalizer === ancestor + ) { + handledInOrder.push({ + name: 'finally', + node: ancestor, + relatedTry: ancestor.parent + }) + } else if ( + ancestor.type === 'FunctionExpression' || + ancestor.type === 'FunctionDeclaration' + ) { + // clear the current parents, we are in a new function + handledInOrder = [] + } + }) + + if (handledInOrder.length === 0) { + return false + } + + let lastItem = handledInOrder[handledInOrder.length - 1] + + while ( + handledInOrder.length > 0 && + !(lastItem.name === 'try' && lastItem.node.handler) + ) { + const tryToBeDeleted = lastItem.relatedTry + + while ( + handledInOrder.length > 0 && + lastItem.relatedTry == tryToBeDeleted + ) { + handledInOrder.pop() + lastItem = handledInOrder[handledInOrder.length - 1] + } + } + + return handledInOrder.length > 0 + } + + return { + AwaitExpression(node) { + if (isAwaitHandled()) { + return + } + + context.report({ + node, + message: '"await"s must be wrapped with a try/catch statement.' + }) + } + } + } +}