Repository: icebob/fastest-validator
Pull request #228 (2021-03-30 15:07:56)

Async support
Hi,

I've added `async` support to the custom validators. It can be useful when you should check some data in the database (e.g. check unique username).

This implementation uses `async/await` because implementing a `Promise` chain would be a horrible job.
**So this new feature works only on environments which supports `async/await`**:
- Chrome > 55
- Firefox > 52
- Edge > 15
- NodeJS > 8.x (or 7.6 with harmony)

There are two new methods: `validateAsync` and `compileAsync`. These methods return `Promise` and you can use `await` keyword in the custom methods.
or you can turn on the async mode in Validation constructor options. In this case, the original `validate` and `compile` methods also return `Promise`
The two way is added to better support the usages in Moleculer, as well _(current Moleculer Validator middleware can't handle if you turn on the `async: true` in constructor)_.

**Example for 3 types of custom functions**
```js
const v = new Validator({
    debug: true,
    useNewCustomCheckerFunction: true,
    async: true
});

// Register a custom 'even' validator
v.add("even", function({ messages }) {
    return {
        source: `
            if (value % 2 != 0)
                ${this.makeError({ type: "evenNumber",  actual: "value", messages })}

            await new Promise(resolve => setTimeout(resolve, 1000));

            return value;
        `
    };
});
v.addMessage("evenNumber", "The '{field}' field must be an even number! Actual: {actual}");

const schema = {
    name: {
        type: "string",
        min: 4,
        max: 25,
        custom: async (v) => {
            await new Promise(resolve => setTimeout(resolve, 1000));
            return v.toUpperCase();
        }
    },

    username: {
        type: "custom",
        custom: async (v) => {
            // E.g. checking in the DB that whether is unique.
            await new Promise(resolve => setTimeout(resolve, 1000));
            return v.trim();
        }
    },

    age: {
        type: "even"
    }
};

const check = v.compile(schema);

(async function() {
    const data = {
        name: "John Doe",
        username: "johndoe   ",
        age: 21
    };
    console.log(await check(data), data);
})();
```

**Benchmark:**
Of course, the performance is affected in `async` mode. It should be noted in the readme.
```
Platform info:
==============
   Windows_NT 10.0.19041 x64
   Node.JS: 12.14.1
   V8: 7.7.299.13-node.16
   Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz × 8

Suite: Simple object
√ validate*                           1,220,837 rps
√ validate with sanitizations*        1,077,153 rps
√ validate with wrong obj*              679,003 rps

   validate*                              0%      (1,220,837 rps)   (avg: 819ns)
   validate with sanitizations*      -11.77%      (1,077,153 rps)   (avg: 928ns)
   validate with wrong obj*          -44.38%        (679,003 rps)   (avg: 1μs)
-----------------------------------------------------------------------
```


**Further tasks:**
- [x] Add unit tests
- [ ] Add to README


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index c152a23..66e6ce8 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,38 +1,39 @@
 module.exports = {
-    root: true,
-    "env": {
-        "node": true,
-        "commonjs": true,
-        "es6": true,
-        "jquery": false,
-        "jest": true,
-        "jasmine": true
-    },
-    "extends": "eslint:recommended",
-    "parserOptions": {
-        "sourceType": "module"
-    },
-    "rules": {
-        "indent": [
-            "warn",
-            "tab"
-        ],
-        "quotes": [
-            "warn",
-            "double"
-        ],
-        "semi": [
-            "error",
-            "always"
-        ],
-        "no-var": [
-            "error"
-        ],
-        "no-console": [
-            "error"
-        ],
-        "no-unused-vars": [
-            "warn"
-        ]
-    }
-};
\ No newline at end of file
+	root: true,
+	"env": {
+		"node": true,
+		"commonjs": true,
+		"es6": true,
+		"jquery": false,
+		"jest": true,
+		"jasmine": true
+	},
+	"extends": "eslint:recommended",
+	"parserOptions": {
+		"sourceType": "module",
+		"ecmaVersion": "2018"
+	},
+	"rules": {
+		"indent": [
+			"warn",
+			"tab"
+		],
+		"quotes": [
+			"warn",
+			"double"
+		],
+		"semi": [
+			"error",
+			"always"
+		],
+		"no-var": [
+			"error"
+		],
+		"no-console": [
+			"error"
+		],
+		"no-unused-vars": [
+			"warn"
+		]
+	}
+};
diff --git a/README.md b/README.md
index 93ccd11..92c9223 100644
--- a/README.md
+++ b/README.md
@@ -42,85 +42,6 @@ $ npm install
 $ npm run bench
 ```
 
-# Table of contents
-
-- [fastest-validator ![NPM version](https://www.npmjs.com/package/fastest-validator) [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=The%20fastest%20JS%20validator%20library%20for%20NodeJS&url=https://github.com/icebob/fastest-validator&via=Icebobcsi&hashtags=nodejs,javascript)](#fastest-validator--)
-  - [Key features](#key-features)
-- [How fast?](#how-fast)
-- [Table of contents](#table-of-contents)
-  - [Installation](#installation)
-    - [NPM](#npm)
-  - [Usage](#usage)
-    - [Validate](#validate)
-    - [Browser usage](#browser-usage)
-    - [Deno usage](#deno-usage)
-    - [Supported frameworks](#supported-frameworks)
-- [Optional, Required & Nullable fields](#optional-required--nullable-fields)
-  - [Optional](#optional)
-  - [Nullable](#nullable)
-- [Strict validation](#strict-validation)
-  - [Remove additional fields](#remove-additional-fields)
-- [Multiple validators](#multiple-validators)
-- [Root element schema](#root-element-schema)
-- [Sanitizations](#sanitizations)
-  - [Default values](#default-values)
-- [Shorthand definitions](#shorthand-definitions)
-    - [Array of X](#array-of-x)
-    - [Nested objects](#nested-objects)
-- [Alias definition](#alias-definition)
-- [Default options](#default-options)
-- [Built-in validators](#built-in-validators)
-  - [`any`](#any)
-  - [`array`](#array)
-    - [Properties](#properties)
-  - [`boolean`](#boolean)
-    - [Properties](#properties-1)
-  - [`class`](#class)
-    - [Properties](#properties-2)
-  - [`currency`](#currency)
-    - [Properties](#properties-3)
-  - [`date`](#date)
-    - [Properties](#properties-4)
-  - [`email`](#email)
-    - [Properties](#properties-5)
-  - [`enum`](#enum)
-    - [Properties](#properties-6)
-  - [`equal`](#equal)
-    - [Properties](#properties-7)
-  - [`forbidden`](#forbidden)
-    - [Properties](#properties-8)
-  - [`function`](#function)
-  - [`luhn`](#luhn)
-  - [`mac`](#mac)
-  - [`multi`](#multi)
-  - [`number`](#number)
-    - [Properties](#properties-9)
-  - [`object`](#object)
-    - [Properties](#properties-10)
-  - [`string`](#string)
-    - [Properties](#properties-11)
-  - [`tuple`](#tuple)
-    - [Properties](#properties-12)
-  - [`url`](#url)
-    - [Properties](#properties-13)
-  - [`uuid`](#uuid)
-    - [Properties](#properties-14)
-  - [`objectID`](#objectid)
-    - [Properties](#properties-15)
-- [Custom validator](#custom-validator)
-  - [Custom validation for built-in rules](#custom-validation-for-built-in-rules)
-- [Custom error messages (l10n)](#custom-error-messages-l10n)
-- [Personalised Messages](#personalised-messages)
-- [Plugins](#plugins)
-- [Message types](#message-types)
-  - [Message fields](#message-fields)
-- [Development](#development)
-- [Test](#test)
-  - [Coverage report](#coverage-report)
-- [Contribution](#contribution)
-- [License](#license)
-- [Contact](#contact)
-
 ## Installation
 
 ### NPM
@@ -174,7 +95,7 @@ console.log("Second:", check({ id: 2, name: "Adam" }));
 ```
 
 ```js
-var v = new FastestValidator();
+const v = new FastestValidator();
 
 const schema = {
     id: { type: "number", positive: true, integer: true },
@@ -393,7 +314,7 @@ const schema = {
 You can set default rule options.
 
 ```js
-var v = new FastestValidator({
+const v = new FastestValidator({
     defaults: {
         object: {
             strict: "remove"
@@ -1232,6 +1153,60 @@ console.log(v.validate({ name: "John", phone: "36-70-123-4567" }, schema));
 
 >Please note: the custom function must return the `value`. It means you can also sanitize it.
 
+## Asynchronous custom validations
+You can also use async custom validators. This can be useful if you need to check something in a database or in a remote location.
+In this case you should use `async/await` keywords, or return a `Promise` in the custom validator functions.
+
+>This implementation uses `async/await` keywords. So this feature works only on environments which [supports async/await](https://caniuse.com/async-functions):
+>
+> - Chrome > 55
+> - Firefox > 52
+> - Edge > 15
+> - NodeJS > 8.x (or 7.6 with harmony)
+
+To enable async mode, you should set `$$async: true` in the root of your schema.
+
+**Example with custom checker function**
+```js
+const v = new Validator({
+    useNewCustomCheckerFunction: true, // using new version
+    messages: {
+        // Register our new error message text
+        unique: "The username is already exist"
+    }
+});
+
+const schema = {
+	$$async: true,
+	name: { type: "string" },
+	username: {
+		type: "string",
+        min: 2,
+		custom: async (v, errors) => {
+			// E.g. checking in the DB that the value is unique.
+			const res = await DB.checkUsername(v);
+            if (!res) 
+                errors.push({ type: "unique", actual: value });
+
+            return v;
+		}
+	}
+    // ...
+};
+
+const check = v.compile(schema);
+
+const res = await check(user);
+console.log("Result:", res);
+```
+
+
+The compiled `check` function contains an `async` property, so  you can check if it returns a `Promise` or not.
+```js
+const check = v.compile(schema);
+console.log("Is async?", check.async);
+```
+
 # Custom error messages (l10n)
 You can set your custom messages in the validator constructor.
 
diff --git a/benchmark/suites/async.js b/benchmark/suites/async.js
new file mode 100644
index 0000000..f26ea19
--- /dev/null
+++ b/benchmark/suites/async.js
@@ -0,0 +1,142 @@
+"use strict";
+
+const Benchmarkify = require("benchmarkify");
+const benchmark = new Benchmarkify("Fastest validator benchmark").printHeader();
+
+let bench = benchmark.createSuite("Simple object");
+
+const Validator = require("../../index");
+const v = new Validator({ async: true });
+
+const obj = {
+	name: "John Doe",
+	email: "john.doe@company.space",
+	firstName: "John",
+	phone: "123-4567",
+	age: 33
+};
+
+const wrongObj = {
+	name: "John Doe",
+	email: "john.doe@company.space",
+	firstName: "John",
+	phone: "123-4567",
+	age: 5
+};
+
+const schema = {
+	name: {
+		type: "string",
+		min: 4,
+		max: 25
+	},
+	email: { type: "email" },
+	firstName: { type: "string" },
+	phone: { type: "string"},
+	age: {
+		type: "number",
+		min: 18
+	}
+};
+
+const schema2 = {
+	name: {
+		type: "string",
+		min: 4,
+		max: 25,
+		messages: {
+			string: "Csak szöveges érték",
+			stringMin: "Túl rövid!",
+			stringMax: "Túl hosszú"
+		}
+	},
+	email: { type: "email" },
+	firstName: { type: "string" },
+	phone: { type: "string"},
+	age: {
+		type: "number",
+		min: 18
+	}
+};
+
+const schema3 = {
+	name: {
+		type: "string",
+		min: 4,
+		max: 25,
+		trim: true
+	},
+	email: { type: "email" },
+	firstName: { type: "string", trim: true },
+	phone: { type: "string", trim: true },
+	age: {
+		type: "number",
+		min: 18,
+		convert: true
+	}
+};
+/*
+bench.ref("compile & validate", () => {
+	const res = v.validate(obj, schema);
+	if (res !== true)
+		throw new Error("Validation error!", res);
+});
+
+bench.add("compile & validate with custom messages", () => {
+	const res = v.validate(obj, schema2);
+	if (res !== true)
+		throw new Error("Validation error!", res);
+});
+*/
+const check = v.compile(schema);
+const check3 = v.compile(schema3);
+
+bench.add("validate", async done => {
+	const res = await check(obj);
+	if (res !== true)
+		throw new Error("Validation error!", res);
+	done();
+});
+
+bench.add("validate with sanitizations", async done => {
+	const res = await check3(obj);
+	if (res !== true)
+		throw new Error("Validation error!", res);
+	done();
+});
+
+bench.add("validate with wrong obj", async done => {
+	const res = check(wrongObj);
+	if (res === true)
+		throw new Error("Validation error!", res);
+	done();
+});
+
+bench.run();
+
+
+/*
+
+===============================
+  Fastest validator benchmark
+===============================
+
+Platform info:
+==============
+   Windows_NT 10.0.19041 x64
+   Node.JS: 12.14.1
+   V8: 7.7.299.13-node.16
+   Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz × 8
+
+Suite: Simple object
+√ validate*                           1,220,837 rps
+√ validate with sanitizations*        1,077,153 rps
+√ validate with wrong obj*              679,003 rps
+
+   validate*                              0%      (1,220,837 rps)   (avg: 819ns)
+   validate with sanitizations*      -11.77%      (1,077,153 rps)   (avg: 928ns)
+   validate with wrong obj*          -44.38%        (679,003 rps)   (avg: 1μs)
+-----------------------------------------------------------------------
+
+
+*/
diff --git a/examples/async.js b/examples/async.js
new file mode 100644
index 0000000..bbb35fe
--- /dev/null
+++ b/examples/async.js
@@ -0,0 +1,59 @@
+const Validator = require("../index");
+
+const v = new Validator({
+	debug: true,
+	useNewCustomCheckerFunction: true
+});
+
+// Register a custom 'even' validator
+v.add("even", function({ messages }) {
+	return {
+		source: `
+            if (value % 2 != 0)
+                ${this.makeError({ type: "evenNumber",  actual: "value", messages })}
+
+			await new Promise(resolve => setTimeout(resolve, 1000));
+
+            return value;
+        `
+	};
+});
+v.addMessage("evenNumber", "The '{field}' field must be an even number! Actual: {actual}");
+
+const schema = {
+	$$async: true,
+	name: {
+		type: "string",
+		min: 4,
+		max: 25,
+		custom: async (v) => {
+			await new Promise(resolve => setTimeout(resolve, 1000));
+			return v.toUpperCase();
+		}
+	},
+
+	username: {
+		type: "custom",
+		custom: async (v) => {
+			// E.g. checking in the DB that whether is unique.
+			await new Promise(resolve => setTimeout(resolve, 1000));
+			return v.trim();
+		}
+	},
+
+	age: {
+		type: "even"
+	}
+};
+
+const check = v.compile(schema);
+console.log("Is async?", check.async);
+
+(async function() {
+	const data = {
+		name: "John Doe",
+		username: "johndoe   ",
+		age: 21
+	};
+	console.log(await check(data), data);
+})();
diff --git a/index.d.ts b/index.d.ts
index b93f63c..997d2bc 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -798,6 +798,12 @@ declare module "fastest-validator" {
 		 */
 		$$strict?: boolean | "remove";
 
+		/**
+		 * Enable asynchronous functionality. In this case the `validate` and `compile` methods return a `Promise`.
+		 * @default false
+		 */
+		$$async?: boolean;
+
 		/**
 		 * Basically the validator expects that you want to validate a Javascript object.
 		 * If you want others, you can define the root level schema.
@@ -936,6 +942,17 @@ declare module "fastest-validator" {
 		toHexString(): string;
 	}
 
+
+	export interface SyncCheckFunction {
+		(value: any): true | ValidationError[]
+		async: false
+	 }
+
+	 export interface AsyncCheckFunction {
+		(value: any): Promise<true | ValidationError[]>
+		async: true
+	 }
+
 	export default class Validator {
 		/**
 		 * List of possible error messages
@@ -1012,7 +1029,7 @@ declare module "fastest-validator" {
 		 */
 		compile<T = any>(
 			schema: ValidationSchema<T> | ValidationSchema<T>[]
-		): (value: any) => true | ValidationError[];
+		): SyncCheckFunction | AsyncCheckFunction;
 
 		/**
 		 * Native validation method to validate obj
@@ -1023,7 +1040,7 @@ declare module "fastest-validator" {
 		validate(
 			value: any,
 			schema: ValidationSchema
-		): true | ValidationError[];
+		): true | ValidationError[] | Promise<true | ValidationError[]>;
 
 		/**
 		 * Get defined in validator rule
diff --git a/lib/rules/array.js b/lib/rules/array.js
index 77f1413..fdf43c3 100644
--- a/lib/rules/array.js
+++ b/lib/rules/array.js
@@ -84,7 +84,7 @@ module.exports = function ({ schema, messages }, path, context) {
 		const itemPath = path + "[]";
 		const rule = this.getRuleFromSchema(schema.items);
 		// eslint-disable-next-line quotes
-		const innerSource = 'arr[i] = context.fn[%%INDEX%%](arr[i], (parentField ? parentField : "") + "[" + i + "]", parent, errors, context)';
+		const innerSource = `arr[i] = ${context.async ? "await " : ""}context.fn[%%INDEX%%](arr[i], (parentField ? parentField : "") + "[" + i + "]", parent, errors, context)`;
 		src.push(this.compileRule(rule, context, itemPath, innerSource, "arr[i]"));
 		src.push(`
 			}
diff --git a/lib/rules/multi.js b/lib/rules/multi.js
index b7a1724..e3469e5 100644
--- a/lib/rules/multi.js
+++ b/lib/rules/multi.js
@@ -19,7 +19,7 @@ module.exports = function({ schema, messages }, path, context) {
 		`);
 
 		const rule = this.getRuleFromSchema(schema.rules[i]);
-		src.push(this.compileRule(rule, context, path, "var tmpVal = context.fn[%%INDEX%%](value, field, parent, errors, context);", "tmpVal"));
+		src.push(this.compileRule(rule, context, path, `var tmpVal = ${context.async ? "await " : ""}context.fn[%%INDEX%%](value, field, parent, errors, context);`, "tmpVal"));
 		src.push(`
 				if (errors.length == errBefore) {
 					hasValid = true;
@@ -33,7 +33,7 @@ module.exports = function({ schema, messages }, path, context) {
 		if (hasValid) {
 			errors.length = prevErrLen;
 		}
-		
+
 		return newVal;
 	`);
 
diff --git a/lib/rules/object.js b/lib/rules/object.js
index a8df585..8eadd88 100644
--- a/lib/rules/object.js
+++ b/lib/rules/object.js
@@ -62,7 +62,7 @@ module.exports = function ({ schema, messages }, path, context) {
 
 			const rule = this.getRuleFromSchema(subSchema[property]);
 			const innerSource = `
-				${safePropName} = context.fn[%%INDEX%%](value, field, parentObj, errors, context);
+				${safePropName} = ${context.async ? "await " : ""}context.fn[%%INDEX%%](value, field, parentObj, errors, context);
 			`;
 			sourceCode.push(this.compileRule(rule, context, newPath, innerSource, safePropName));
 		}
@@ -107,12 +107,12 @@ module.exports = function ({ schema, messages }, path, context) {
 		//	- if no strict is set, we need them anyway.
 		if (schema.strict) {
 			sourceCode.push(`
-				props = Object.keys(${subSchema ? 'parentObj' : 'value'});
+				props = Object.keys(${subSchema ? "parentObj" : "value"});
 			`);
 		} else {
 			sourceCode.push(`
-				var props = Object.keys(${subSchema ? 'parentObj' : 'value'});
-				${subSchema ? 'field = parentField;' : ''}
+				var props = Object.keys(${subSchema ? "parentObj" : "value"});
+				${subSchema ? "field = parentField;" : ""}
 			`);
 		}
 	}
diff --git a/lib/rules/tuple.js b/lib/rules/tuple.js
index 11251b1..1957708 100644
--- a/lib/rules/tuple.js
+++ b/lib/rules/tuple.js
@@ -59,7 +59,7 @@ module.exports = function ({ schema, messages }, path, context) {
 			const itemPath = `${path}[${i}]`;
 			const rule = this.getRuleFromSchema(schema.items[i]);
 			const innerSource = `
-			arr[${i}] = context.fn[%%INDEX%%](arr[${i}], (parentField ? parentField : "") + "[" + ${i} + "]", parent, errors, context);
+			arr[${i}] = ${context.async ? "await " : ""}context.fn[%%INDEX%%](arr[${i}], (parentField ? parentField : "") + "[" + ${i} + "]", parent, errors, context);
 		`;
 			src.push(this.compileRule(rule, context, itemPath, innerSource, `arr[${i}]`));
 		}
diff --git a/lib/validator.js b/lib/validator.js
index a0ca2fa..284e5b0 100644
--- a/lib/validator.js
+++ b/lib/validator.js
@@ -1,5 +1,7 @@
 "use strict";
 
+const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
+
 //const flatten = require("./helpers/flatten");
 const deepExtend = require("./helpers/deep-extend");
 
@@ -106,7 +108,7 @@ class Validator {
 			// We should set default-value when value is undefined or null, not skip! (Except when null is allowed)
 			skipUndefinedValue = false;
 			if (rule.schema.nullable !== true) skipNullValue = false;
-			
+
 			let defaultValue;
 			if (typeof rule.schema.default === "function") {
 				if (!context.customs[rule.index]) context.customs[rule.index] = {};
@@ -120,7 +122,7 @@ class Validator {
 				value = ${defaultValue};
 				${resVar} = value;
 			`;
-			
+
 		} else {
 			handleNoValue = this.makeError({ type: "required", actual: "value", messages: rule.messages });
 		}
@@ -149,11 +151,13 @@ class Validator {
 		const self = this;
 		const context = {
 			index: 0,
+			async: schema.$$async === true,
 			rules: [],
 			fn: [],
 			customs: {}
 		};
 		this.cache.clear();
+		delete schema.$$async;
 
 		if (schema.$$root !== true) {
 			if (Array.isArray(schema)) {
@@ -178,7 +182,7 @@ class Validator {
 		];
 
 		const rule = this.getRuleFromSchema(schema);
-		sourceCode.push(this.compileRule(rule, context, null, "context.fn[%%INDEX%%](value, field, null, errors, context);", "value"));
+		sourceCode.push(this.compileRule(rule, context, null, `${context.async ? "await " : ""}context.fn[%%INDEX%%](value, field, null, errors, context);`, "value"));
 
 		sourceCode.push("if (errors.length) {");
 		sourceCode.push(`
@@ -198,7 +202,8 @@ class Validator {
 
 		const src = sourceCode.join("\n");
 
-		const checkFn = new Function("value", "context", src);
+		const FnClass = context.async ? AsyncFunction : Function;
+		const checkFn = new FnClass("value", "context", src);
 
 		/* istanbul ignore next */
 		if (this.opts.debug) {
@@ -212,10 +217,12 @@ class Validator {
 
 		this.cache.clear();
 
-		return function (data) {
+		const resFn = function (data) {
 			context.data = data;
 			return checkFn.call(self, data, context);
 		};
+		resFn.async = context.async;
+		return resFn;
 	}
 
 	/**
@@ -255,7 +262,8 @@ class Validator {
 			context.index++;
 			const res = rule.ruleFunction.call(this, rule, path, context);
 			res.source = res.source.replace(/%%INDEX%%/g, rule.index);
-			const fn = new Function("value", "field", "parent", "errors", "context", res.source);
+			const FnClass = context.async ? AsyncFunction : Function;
+			const fn = new FnClass("value", "field", "parent", "errors", "context", res.source);
 			context.fn[rule.index] = fn;
 			sourceCode.push(this.wrapRequiredCheckSourceCode(rule, innerSrc.replace(/%%INDEX%%/g, rule.index), context, resVar));
 			sourceCode.push(this.makeCustomValidator({vName: resVar, path: customPath, schema: rule.schema, context, messages: rule.messages, ruleIndex: rule.index}));
@@ -321,7 +329,7 @@ class Validator {
 
 		return rule;
 	}
-   
+
 	/**
 	 * Parse rule from shorthand string
 	 * @param {String} str shorthand string
@@ -410,11 +418,11 @@ class Validator {
 
 			if (this.opts.useNewCustomCheckerFunction) {
 				return `
-               const ${ruleVName} = context.customs[${ruleIndex}];
+               		const ${ruleVName} = context.customs[${ruleIndex}];
 					const ${fnCustomErrorsVName} = [];
-					${vName} = ${ruleVName}.schema.${fnName}.call(this, ${vName}, ${fnCustomErrorsVName} , ${ruleVName}.schema, "${path}", parent, context);
+					${vName} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}.call(this, ${vName}, ${fnCustomErrorsVName} , ${ruleVName}.schema, "${path}", parent, context);
 					if (Array.isArray(${fnCustomErrorsVName} )) {
-                  ${fnCustomErrorsVName} .forEach(err => errors.push(Object.assign({ message: ${ruleVName}.messages[err.type], field }, err)));
+                  		${fnCustomErrorsVName} .forEach(err => errors.push(Object.assign({ message: ${ruleVName}.messages[err.type], field }, err)));
 					}
 				`;
 			}
@@ -422,7 +430,7 @@ class Validator {
 			const result = "res_" + ruleVName;
 			return `
 				const ${ruleVName} = context.customs[${ruleIndex}];
-				const ${result} = ${ruleVName}.schema.${fnName}.call(this, ${vName}, ${ruleVName}.schema, "${path}", parent, context);
+				const ${result} = ${context.async ? "await " : ""}${ruleVName}.schema.${fnName}.call(this, ${vName}, ${ruleVName}.schema, "${path}", parent, context);
 				if (Array.isArray(${result})) {
 					${result}.forEach(err => errors.push(Object.assign({ message: ${ruleVName}.messages[err.type], field }, err)));
 				}


Test patch:
diff --git a/test/integration.spec.js b/test/integration.spec.js
index 6bc41b4..7ee3be9 100644
--- a/test/integration.spec.js
+++ b/test/integration.spec.js
@@ -994,15 +994,15 @@ describe("Test default value sanitizer", () => {
 			age: { type: "number", optional: true, default: 33 },
 			roles: { type: "array", items: "string", default: ["user"] },
 			status: { type: "boolean", default: true },
-			tuple: { type: 'tuple', items: [{type: 'number', default: 666}, {type: 'string', default: 'lucifer'}] },
-			array: { type: 'array', items: {type: 'string', default: 'bar'}},
+			tuple: { type: "tuple", items: [{type: "number", default: 666}, {type: "string", default: "lucifer"}] },
+			array: { type: "array", items: {type: "string", default: "bar"}},
 		};
 		const check = v.compile(schema);
 		const obj = {
 			name: null,
 			status: false,
 			tuple: [undefined, undefined],
-			array: ['foo', undefined, 'baz']
+			array: ["foo", undefined, "baz"]
 		};
 
 		const res = check(obj);
@@ -1014,8 +1014,8 @@ describe("Test default value sanitizer", () => {
 			age: 33,
 			roles: ["user"],
 			status: false,
-			tuple: [666, 'lucifer'],
-			array: ['foo', 'bar', 'baz']
+			tuple: [666, "lucifer"],
+			array: ["foo", "bar", "baz"]
 		});
 	});
 
@@ -1180,3 +1180,83 @@ describe("Test nullable option", () => {
 		expect(o2.foo).toBe(5);
 	});
 });
+
+describe("Test async mode", () => {
+	const v = new Validator({ useNewCustomCheckerFunction: true });
+
+	// Async mode 1
+	const custom1 = jest.fn(async value => {
+		await new Promise(resolve => setTimeout(resolve, 100));
+		return value.toUpperCase();
+	});
+
+	// Async mode 2
+	const custom2 = jest.fn(async (value) => {
+		await new Promise(resolve => setTimeout(resolve, 100));
+		return value.trim();
+	});
+
+	// Async mode 3
+	v.add("even", function({ messages }) {
+		return {
+			source: `
+				if (value % 2 != 0)
+					${this.makeError({ type: "evenNumber",  actual: "value", messages })}
+
+				await new Promise(resolve => setTimeout(resolve, 100));
+
+				return value;
+			`
+		};
+	});
+	v.addMessage("evenNumber", "The '{field}' field must be an even number! Actual: {actual}");
+
+	const schema = {
+		$$async: true,
+		id: { type: "number", positive: true },
+		name: { type: "string", custom: custom1 },
+		username: {	type: "custom",	custom: custom2	},
+		age: { type: "even" }
+	};
+	const check = v.compile(schema);
+
+	it("should be async", () => {
+		expect(check.async).toBe(true);
+	});
+
+	it("should call custom async validators", async () => {
+		let obj = {
+			id: 3,
+			name: "John",
+			username: "   john.doe  ",
+			age: 30
+		};
+
+		const res = await check(obj);
+		expect(res).toBe(true);
+
+		expect(custom1).toBeCalledTimes(1);
+		expect(custom1).toBeCalledWith("John", [], schema.name, "name", null, expect.anything());
+
+		expect(custom2).toBeCalledTimes(1);
+		expect(custom2).toBeCalledWith("   john.doe  ", [], schema.username, "username", null, expect.anything());
+	});
+
+	it("should give errors", async () => {
+		const obj = {
+			id: 3,
+			name: "John",
+			username: "   john.doe  ",
+			age: 31
+		};
+
+		const res = await check(obj);
+
+		expect(res.length).toBe(1);
+		expect(res[0].type).toBe("evenNumber");
+		expect(res[0].field).toBe("age");
+		expect(res[0].actual).toBe(31);
+		expect(res[0].expected).toBe(undefined);
+	});
+
+});
diff --git a/test/typescript/integration.spec.ts b/test/typescript/integration.spec.ts
index 6706f51..bb6cf12 100644
--- a/test/typescript/integration.spec.ts
+++ b/test/typescript/integration.spec.ts
@@ -1204,3 +1204,84 @@ describe('TypeScript Definitions', () => {
     });
 })
 
+describe("Test async mode", () => {
+	const v = new Validator({ useNewCustomCheckerFunction: true });
+
+	// Async mode 1
+	const custom1 = jest.fn(async value => {
+		await new Promise(resolve => setTimeout(resolve, 100));
+		return value.toUpperCase();
+	});
+
+	// Async mode 2
+	const custom2 = jest.fn(async (value) => {
+		await new Promise(resolve => setTimeout(resolve, 100));
+		return value.trim();
+	});
+
+	// Async mode 3
+	v.add("even", function(this: ValidatorType, { messages }) {
+		return {
+			source: `
+				if (value % 2 != 0)
+					${this.makeError({ type: "evenNumber",  actual: "value", messages })}
+
+				await new Promise(resolve => setTimeout(resolve, 100));
+
+				return value;
+			`
+		};
+	});
+	v.addMessage("evenNumber", "The '{field}' field must be an even number! Actual: {actual}");
+
+	const schema = {
+		$$async: true,
+		id: { type: "number", positive: true },
+		name: { type: "string", custom: custom1 },
+		username: {	type: "custom",	custom: custom2	},
+		age: { type: "even" }
+	};
+	const check = v.compile(schema);
+
+	it("should be async", () => {
+		expect(check.async).toBe(true);
+	});
+
+	it("should call custom async validators", async () => {
+		let obj = {
+			id: 3,
+			name: "John",
+			username: "   john.doe  ",
+			age: 30
+		};
+
+		const res = await check(obj);
+		expect(res).toBe(true);
+
+		expect(custom1).toBeCalledTimes(1);
+		expect(custom1).toBeCalledWith("John", [], schema.name, "name", null, expect.anything());
+
+		expect(custom2).toBeCalledTimes(1);
+		expect(custom2).toBeCalledWith("   john.doe  ", [], schema.username, "username", null, expect.anything());
+	});
+
+	it("should give errors", async () => {
+		const obj = {
+			id: 3,
+			name: "John",
+			username: "   john.doe  ",
+			age: 31
+		};
+
+		const res = await check(obj);
+
+		expect(res).toEqual([{
+			type: "evenNumber",
+			field: "age",
+			actual: 31,
+			expected: undefined,
+			message: "The 'age' field must be an even number! Actual: 31",
+		}]);
+	});
+
+});
diff --git a/test/typescript/validator.spec.ts b/test/typescript/validator.spec.ts
index bc05bb8..3ef59fc 100644
--- a/test/typescript/validator.spec.ts
+++ b/test/typescript/validator.spec.ts
@@ -120,7 +120,8 @@ describe('TypeScript Definitions', () => {
                 customs: expect.any(Object),
                 rules: expect.any(Array),
                 fn: expect.any(Array),
-                index: 2
+                index: 2,
+				async: false
             };
 
             expect(validFn).toHaveBeenCalledTimes(1);
diff --git a/test/validator.spec.js b/test/validator.spec.js
index a9ebb32..0bafe68 100644
--- a/test/validator.spec.js
+++ b/test/validator.spec.js
@@ -158,7 +158,8 @@ describe("Test add", () => {
 			customs: expect.any(Object),
 			rules: expect.any(Array),
 			fn: expect.any(Array),
-			index: 2
+			index: 2,
+			async: false
 		};
 
 
@@ -496,7 +497,7 @@ describe("Test custom validation v1", () => {
 					fn(value);
 					if (value % 2 !== 0) return [{ type: "evenNumber", actual: value }];
 				}
-			}, 
+			},
 			b: {
 				type: "number",
 				custom(value){