How to find missing Await on Async function calls in Node+Typescript+VSCode? - node.js

We've deployed bugs in our node app b/c we forgot to prefix async function calls with "await".
Example:
const getUsers = async () => db.query('SELECT * from Users');
const testMissingAwait = async () => {
const users = getUsers(); // <<< missing await
console.log(users.length);
};
testMissingAwait();
Is there an easy way to find async function calls missing the await keyword?
Failing that, how much effort would it be to write a Visual Studio Code extension that flags these automatically? (I'm up for tackling if anyone can point me in the right direction).

TypeScript compiler doesn't provide a compiler option for that. However, TSLint 4.4 provides an option to detect floating promises. You can read this blog post for more detailed answer: Detect missing await in typescript
Download TSLint:
npm install -g tslint typescript
Configure TSLint:
{
"extends": "tslint:recommended",
"rules": {
"no-floating-promises": true
}
}
Run TSLint:
tslint --project tsconfig.json
If there are floating promises in your code, you should see the following error:
ERROR: F:/Samples/index.ts[12, 5]: Promises must be handled appropriately

TypeScript already does this
// users is a Promise<T>
const users = getUsers(); // <<< missing await
// users.length is not a property of users... then and catch are
console.log(users.length);
You can find situations where you won't be told about your mistake - where the types are compatible, for example I missed an await here:
function delay(ms: number) {
return new Promise<number>(function(resolve) {
setTimeout(() => {
resolve(5);
}, ms);
});
}
async function asyncAwait() {
let x = await delay(1000);
console.log(x);
let y = delay(1000);
console.log(y);
return 'Done';
}
asyncAwait().then((result) => console.log(result));
Because console.log doesn't cause any type incompatibility between my numbers and my promises, the compiler can't tell I have made a mistake.
The only solution here would be a type annotation... but if you're gonna forget an await, you're just as likely to forget a type annotation.
let y: number = delay(1000);

tslint is deprecated but typescript-eslint has a rule for this: no-floating-promises
This rule forbids usage of Promise-like values in statements without handling their errors appropriately ... Valid ways of handling a Promise-valued statement include awaiting, returning, and either calling .then() with two arguments or .catch() with one argument.
To use it, you'll need to set up typescript-eslint:
Install dependencies
npm install --save-dev eslint #typescript-eslint/parser #typescript-eslint/eslint-plugin
Modify your .eslintrc file
These are the minimum required settings; see the documentation for more options (https://typescript-eslint.io/docs/linting/linting):
{
"parser": "#typescript-eslint/parser",
"parserOptions": { "project": "./tsconfig.json" },
"plugins": ["#typescript-eslint"],
"rules": {
"#typescript-eslint/no-floating-promises": ["error"]
}
}
If there are places where you want to call an async function without using await, you can either:
Use the void operator as mentioned in the documentation for the rule, e.g.
void someAsyncFunction();
Or just change error to warn in the .eslintrc configuration above
Next time you run eslint you should see the rule applied:
$ npm run lint
...
./src/database/database.ts
219:7 warning Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator #typescript-eslint/no-floating-promises
Since you mentioned VS Code specifically, this also integrates great with the ESLint plugin:

Related

using ESM imports in functions

when you add imports to an ESM package, you can resolve alias paths into the corresponding real ones, like this:
package.json
{
"type": "module",
"imports": { "#*": "./*" }
...
}
now we can use it like this import something from "#module" which resolves into import something from "./module"
but, I can't see any docs about using the same resolving mechanism for builtin functions (or maybe user functions either) that accept PathLike arguments like readFileSync
readFileSync("#file.txt")
I know it is a module resolving, but we can use it as a path resolving.
my question is: how to achieve the same mechanism to resolve any PathLike path, without a manual modification, because simply we need to dispense the relative paths
an example of manual resolving
function example(path: PathLike){
let imports = /* read the property imports from package.json */
let keys = Object.keys(imports)
// make a loop, and for each key make something like this (needs additional work)
let resolvedPath = path.replace('#','./')
// now, use resolvedPath instead of path
}
example of nodejs resolving
function example2(path: PathLike){
let resolvedPath = resolve(path)
}
in addition to the original work of resolve() it also uses the property import to resolve the path to avoid using relative paths
I ask if there is a way to achieve this goal.
The solution
thanks to #RickN this is the final solution:
export function resolveImports(path: PathLike): Promise<string> {
return import.meta!.resolve!(path.toString()).then((resolvedPath) =>
fileURLToPath(resolvedPath)
);
}
As of writing, you need a CLI flag to enable the resolve method in import.meta:
node --experimental-import-meta-resolve your-file.js
(The first part of the name speaks for itself.)
Then, in a script:
console.log( await import.meta.resolve('#foo/bar.js') );
// It can resolve any type of file, even if you can't import() it.
console.log( await import.meta.resolve('#foo/hello.txt') );
As it's a promise-based function, you'll need to await the result.
async function example2(path: PathLike){
return import.meta.resolve(path);
}
As a side-note: this returns a URL (file:///...) so run the result through the built-in URL class url.fileURLToPath() if you just want a file path:
import { fileURLToPath } from 'node:url';
// OR: const { fileURLToPath } = require('node:url');
// For older node versions use `require('url')`
console.log( fileURLToPath(await import.meta.resolve('#foo/hello.txt')) );
// => /tmp/example/directory-with-modules-in-it/hello.txt

Problem with compile PKG to exe: Babel parse has failed: 'await' is only allowed within async functions and at the top levels of modules

I wrote a script that works with puppeteer and with async/await. I specified the type as a module because it supports async/await without problems. But when compiling to exe with PKG I get an error:
pkg#5.8.0
Warning Babel parse has failed: 'await' is only allowed within async functions and at the top levels of modules. (6:8)
Warning Failed to make bytecode node16-x64 for file C:\snapshot\my-script\main.js
Tried solutions from the links:
topLevelAwait invalid with babel-loader: 'await' is only allowed within async functions
And a few more...
Basically, everyone advises using promises, but firstly, I'm new to node js and secondly, I need to do it with "crutches" in the whole script
I even tried to do it on different computers) The result is the same.
I just don't understand what I'm doing wrong. I wrote a small script for the test, it also gives this error when trying to compile..
main.js
async function Sum(x, y)
{
return x + y;
}
let x = await Sum(1, 3);
console.log(x);
package.json
{
"name": "Test",
"type": "module",
"main": "main.js",
"devDependencies": {
"#babel/plugin-syntax-top-level-await": "^7.14.5"
}
}

Stub an export from a native ES Module without babel

I'm using AVA + sinon to build my unit test. Since I need ES6 modules and I don't like babel, I'm using mjs files all over my project, including the test files. I use "--experimental-modules" argument to start my project and I use "esm" package in the test. The following is my ava config and the test code.
"ava": {
"require": [
"esm"
],
"babel": false,
"extensions": [
"mjs"
]
},
// test.mjs
import test from 'ava';
import sinon from 'sinon';
import { receiver } from '../src/receiver';
import * as factory from '../src/factory';
test('pipeline get called', async t => {
const stub_factory = sinon.stub(factory, 'backbone_factory');
t.pass();
});
But I get the error message:
TypeError {
message: 'ES Modules cannot be stubbed',
}
How can I stub an ES6 module without babel?
According to John-David Dalton, the creator of the esm package, it is only possible to mutate the namespaces of *.js files - *.mjs files are locked down.
That means Sinon (and all other software) is not able to stub these modules - exactly as the error message points out. There are two ways to fix the issue here:
Just rename the files' extension to .js to make the exports mutable. This is the least invasive, as the mutableNamespace option is on by default for esm. This only applies when you use the esm loader, of course.
Use a dedicated module loader that proxies all the imports and replaces them with one of your liking.
The tech stack agnostic terminology for option 2 is a link seam - essentially replacing Node's default module loader. Usually one could use Quibble, ESMock, proxyquire or rewire, meaning the test above would look something like this when using Proxyquire:
// assuming that `receiver` uses `factory` internally
// comment out the import - we'll use proxyquire
// import * as factory from '../src/factory';
// import { receiver } from '../src/receiver';
const factory = { backbone_factory: sinon.stub() };
const receiver = proxyquire('../src/receiver', { './factory' : factory });
Modifying the proxyquire example to use Quibble or ESMock (both supports ESM natively) should be trivial.
Sinon needs to evolve with the times or be left behind (ESM is becoming defacto now with Node 12) as it is turning out to be a giant pain to use due to its many limitations.
This article provides a workaround (actually 4, but I only found 1 to be acceptable). In my case, I was exporting functions from a module directly and getting this error: ES Modules cannot be stubbed
export function abc() {
}
The solution was to put the functions into a class and export that instead:
export class Utils {
abc() {
}
}
notice that the function keyword is removed in the method syntax.
Happy Coding - hope Sinon makes it in the long run, but it's not looking good given its excessive rigidity.
Sticking with the questions Headline „Stub an export from a native ES Module without babel“ here's my take, using mocha and esmock:
(credits: certainly #oligofren brought me on the right path…)
package.json:
"scripts": {
...
"test": "mocha --loader=esmock",
"devDependencies": {
"esmock": "^2.1.0",
"mocha": "^10.2.0",
TestDad.js (a class)
import { sonBar } from './testSon.js'
export default class TestDad {
constructor() {
console.log(purple('constructing TestDad, calling...'))
sonBar()
}
}
testSon.js (a 'util' library)
export const sonFoo = () => {
console.log(`Original Son 'foo' and here, my brother... `)
sonBar()
}
export const sonBar = () => {
console.log(`Original Son bar`)
}
export default { sonFoo, sonBar }
esmockTest.js
import esmock from 'esmock'
describe.only(autoSuiteName(import.meta.url),
() => {
it('Test 1', async() => {
const TestDad = await esmock('../src/commands/TestDad.js', {
'../src/commands/testSon.js': {
sonBar: () => { console.log('STEPSON Bar') }
}
})
// eslint-disable-next-line no-new
new TestDad()
})
it('Test 2', async() => {
const testSon = await esmock('../src/commands/testSon.js')
testSon.sonBar = () => { console.log('ANOTHER STEPSON Bar') }
testSon.sonFoo() // still original
testSon.sonBar() // different now
})
})
autoSuiteName(import.meta.url)
regarding Test1
working nicely, import bended as desired.
regarding Test1
Bending a single function to do something else is not a problem.
(but then there is not much test value in calling your very own function you just defined, is there?)
Enclosed function calls within the module (i.e. from sonFoo to sonBar) remain what they are, they are indeed a closure, still pointing to the prior function
Btw also tested that: No better results with sinon.callsFake() (would have been surprising if there was…)

node require.cache delete does not result in reload of module

I'm writing tests for my npm module.
These tests require to install multiple versions of an npm module in order to check if the module will validate them as compatible or incompatible.
Somehow all uncache libraries or function I found on stackoverflow or the npm database are not working..
I install/uninstall npm modules by using my helper functions:
function _run_cmd(cmd, args) {
return new Promise((res, rej) => {
const child = spawn(cmd, args)
let resp = ''
child.stdout.on('data', function (buffer) {
resp += buffer.toString()
})
child.stdout.on('end', function() {
res(resp)
})
child.stdout.on('error', (err) => rej(err))
})
}
global.helper = {
npm: {
install: function (module) {
return _run_cmd('npm', ['install', module])
},
uninstall: function (module) {
decacheModule(module)
return _run_cmd('npm', ['uninstall', module])
}
}
}
This is my current decache function which should clear all modules caches (I tried others, including npm modules none of them worked)
function decacheModules() {
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key]
})
}
I am installing multiple versions of the less module (https://www.npmjs.com/package/less)
In my first test I am installing a deprecated version which does not have a render-function.
In some other test I am installing an up-to-date version which has the render-function. Somehow if I test for that function that test does fail.
If I skip the first test the other test succeeds. (render-function exists).
This makes me believe that the deletion of require.cache has no impact...
I am using node v4.2.4.
If there is a reference to the old module:
var less = require('less');
Clear module cache will not lead that reference cleared and reload the module.
For that to work, at least you don't store module to variable, use the "require('less')" in place everywhere.

Is there a jest config that will fail tests on console.warn?

How do I configure jest tests to fail on warnings?
console.warn('stuff');
// fail test
You can use this simple override :
let error = console.error
console.error = function (message) {
error.apply(console, arguments) // keep default behaviour
throw (message instanceof Error ? message : new Error(message))
}
You can make it available across all tests using Jest setupFiles.
In package.json :
"jest": {
"setupFiles": [
"./tests/jest.overrides.js"
]
}
Then put the snippet into jest.overrides.js
For those using create-react-app, not wanting to run npm run eject, you can add the following code to ./src/setupTests.js:
global.console.warn = (message) => {
throw message
}
global.console.error = (message) => {
throw message
}
Now, jest will fail when messages are passed to console.warn or console.error.
create-react-app Docs - Initializing Test Environment
I implemented this recently using jest.spyOn introduced in v19.0.0 to mock the warn method of console (which is accesses via the global context / object).
Can then expect that the mocked warn was not called, as shown below.
describe('A function that does something', () => {
it('Should not trigger a warning', () => {
var warn = jest.spyOn(global.console, 'warn');
// Do something that may trigger warning via `console.warn`
doSomething();
// ... i.e.
console.warn('stuff');
// Check that warn was not called (fail on warning)
expect(warn).not.toHaveBeenCalled();
// Cleanup
warn.mockReset();
warn.mockRestore();
});
});
There is a useful npm package that helps you to achieve that: jest-fail-on-console
It's easily configurable.
Install:
npm i -D jest-fail-on-console
Configure:
In a file used in the setupFilesAfterEnv option of Jest, add this code:
import failOnConsole from 'jest-fail-on-console'
failOnConsole()
// or with options:
failOnConsole({ shouldFailOnWarn: false })
I decided to post a full example based on user1823021 answer
describe('#perform', () => {
var api
// the global.fetch is set to jest.fn() object globally
global.fetch = jest.fn()
var warn = jest.spyOn(global.console, 'warn');
beforeEach(function() {
// before every test, all mocks need to be resetted
api = new Api()
global.fetch.mockReset()
warn.mockReset()
});
it('triggers an console.warn if fetch fails', function() {
// In this test fetch mock throws an error
global.fetch.mockImplementationOnce(() => {
throw 'error triggered'
})
// I run the test
api.perform()
// I verify that the warn spy has been triggered
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toBeCalledWith("api call failed with error: ", "error triggered")
});
it('calls fetch function', function() {
// I create 2 more mock objects to verify the fetch parameters
const url = jest.fn()
const config = jest.fn()
api.url = url
api.config = config
// I run the test
api.perform()
// I verify that fetch has been called with url and config mocks
expect(global.fetch).toHaveBeenCalledTimes(1)
expect(global.fetch).toBeCalledWith(url, config)
expect(warn).toHaveBeenCalledTimes(0)
});
})
the #perform method I am testing
class Api {
constructor(auth) {
this._credentials = auth
}
perform = async () => {
try {
return await fetch(this.url, this.config)
} catch(error) {
console.warn('api call failed with error: ', error)
}
}
}
You can set the environment variable CI=true before running jest which will cause it to fail tests on warnings in addition to errors.
Example which runs all test files in the test folder:
CI=true jest ./test
Automated CI/CD pipelines such as Github Actions set CI to true by default, which can be one reason why a unit test will pass on your local machine when warnings are thrown, but fail in the pipeline.
(Here is the Github Actions documentation on default environment variables: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables)

Resources