diff --git a/composer.json b/composer.json
index 3cfdf384863d2230dac948ae39bf5eaf4e009ec0..0dcea46a21c1f528333c818b6d25d793f5efcefb 100644
--- a/composer.json
+++ b/composer.json
@@ -18,6 +18,9 @@
 		"psr-4": {
 			"Grifart\\AssertFunction\\": "src"
 		},
+		"classmap": [
+			"src/exceptions.php"
+		],
 		"files": [
 			"src/functions.php"
 		]
@@ -26,7 +29,10 @@
 	"autoload-dev": {
 		"psr-4": {
 			"Grifart\\AssertFunction\\": "tests"
-		}
+		},
+		"files": [
+			"src/exceptions.php"
+		]
 	}
 
 }
diff --git a/src/exceptions.php b/src/exceptions.php
new file mode 100644
index 0000000000000000000000000000000000000000..ab8b81dee6d4795d8be3dc0d20b7c7b7989b2994
--- /dev/null
+++ b/src/exceptions.php
@@ -0,0 +1,38 @@
+<?php declare(strict_types=1);
+namespace Grifart\AssertFunction;
+
+use Throwable;
+
+final class AssertFunctionException extends \LogicException {
+
+	/** @internal use named constructors instead */
+	public function __construct($message, \Throwable $previous = NULL)
+	{
+		parent::__construct($message, 0, $previous);
+	}
+
+	private static function location(\ReflectionFunction $reflection): string
+	{
+		return $reflection->getFileName() . ':' . $reflection->getStartLine() . '-' . $reflection->getEndLine() . ' ';
+	}
+
+
+	public static function cannotStartReflection(\ReflectionException $exception): self
+	{
+		return new self('Cannot start reflection for given function.', 0, $exception);
+	}
+
+
+	public static function wrongNumberOrArguments(\ReflectionFunction $reflection, int $numberOfParameters, int $actualCount): self
+	{
+		return new self(self::location($reflection) . "Given wrong number of parameters. Expected $numberOfParameters, $actualCount given.");
+	}
+
+
+	public static function missingReturnType(\ReflectionFunction $reflection, string $type): self
+	{
+		return new self(self::location($reflection) . "Function is required to have return type of type $type.");
+	}
+};
+
+abstract class RuntimeException extends \RuntimeException {};
diff --git a/src/functions.php b/src/functions.php
index aca22c98e3a7b639bee91a14ce1b8b0279aa7846..ed9a64f4d1851cae0630c5223e142ac282b8d70f 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -3,6 +3,11 @@
  * Used to load functions.
  */
 
+// todo: use only assert( ... ) instead of exceptions; to be compatible without configuration with PHP env
+// todo: [performance] separate code itself into autoloaded class
+// todo: better assertion messages
+// todo: covariance and contra-variance
+
 namespace Grifart\AssertFunction;
 
 function parameters(string ...$params): array
@@ -15,7 +20,68 @@ function optional(string $classType): string
 	return '?' . $classType;
 }
 
-function assertFunction(callable $fn, array $parameters, string $returnType): void
+function assertFunction(callable $function, array $parameters, ?string $expectedReturnType): void
+{
+	try {
+		$reflection = new \ReflectionFunction($function);
+	} catch (\ReflectionException $exception) {
+		throw AssertFunctionException::cannotStartReflection($exception);
+	}
+
+	// PARAMETERS CHECK:
+	$numberOfParameters = $reflection->getNumberOfParameters();
+	if ($numberOfParameters !== count($parameters)) {
+		throw AssertFunctionException::wrongNumberOrArguments($reflection, count($parameters), $numberOfParameters);
+	}
+
+	$i = 0;
+	/** @var string[] $parameters */
+	foreach($parameters AS $parameter) {
+		assert(is_string($parameter));
+
+		$parameterReflection = $reflection->getParameters()[$i++];
+		assert($parameterReflection instanceof \ReflectionParameter);
+
+		__checkFunctionParameter($parameterReflection, ...__parseType($parameter)); // todo: move to helper class
+	}
+
+
+	// RETURN TYPE:
+	if($expectedReturnType !== NULL) {
+
+		if (!$reflection->hasReturnType()) {
+			throw AssertFunctionException::missingReturnType($reflection, $expectedReturnType);
+		}
+
+		list($_returnType, $_optional) = __parseType($expectedReturnType);
+		$returnTypeReflection = $reflection->getReturnType();
+
+		assert($_optional === $returnTypeReflection->allowsNull());
+		assert($_returnType === (string) $reflection->getReturnType());
+	}
+}
+
+function __parseType(string $type): array
+{
+	$cleanType = $type;
+	$optional = FALSE;
+	if ($type[0] === '?') {
+		$optional = TRUE;
+		$cleanType = substr($type, 1); // without leading ?
+	}
+	return [$cleanType, $optional];
+}
+
+function __checkFunctionParameter(
+	\ReflectionParameter $parameterReflection,
+	string $parameterType,
+	bool $optional
+): void
 {
+	$reflectionType = $parameterReflection->getType();
+	assert($reflectionType instanceof \ReflectionType);
 
+	// checks:
+	assert($optional === $reflectionType->allowsNull());
+	assert($parameterType === (string) $reflectionType);
 }
diff --git a/tests/fn.assertFunction.allOptional.phpt b/tests/fn.assertFunction.allOptional.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..8438042821bd373885f7a15e5c454c614cc502d5
--- /dev/null
+++ b/tests/fn.assertFunction.allOptional.phpt
@@ -0,0 +1,32 @@
+<?php declare(strict_types=1);
+namespace MyTestNamespace;
+require __DIR__ . '/bootstrap.php';
+require __DIR__ . '/testClasses.php';
+use function Grifart\AssertFunction\{assertFunction, optional, parameters};
+use Tester\Assert;
+
+$fn = function(?T1 $t1, ?T2 $t2): ?T3 {return new T3;};
+assertFunction($fn, parameters(optional(T1::class), optional(T2::class)), optional(T3::class));
+
+// missing optional check
+Assert::exception(function () use ($fn) {
+	assertFunction($fn, parameters(T1::class, optional(T2::class)), optional(T3::class));
+}, \AssertionError::class /* todo: assert message */);
+
+Assert::exception(function () use ($fn) {
+	assertFunction($fn, parameters(optional(T1::class), T2::class), optional(T3::class));
+}, \AssertionError::class /* todo: assert message */);
+
+Assert::exception(function () use ($fn) {
+	assertFunction($fn, parameters(optional(T1::class), optional(T2::class)), T3::class);
+}, \AssertionError::class /* todo: assert message */);
+
+// Wrong type return type
+Assert::exception(function () use ($fn) {
+	assertFunction($fn, parameters(optional(T1::class), optional(T2::class)), optional(T2::class));
+}, \AssertionError::class /* todo: assert message */);
+
+// Wrong parameter type
+Assert::exception(function () use ($fn) {
+	assertFunction($fn, parameters(optional(T1::class), optional(T1::class)), optional(T3::class));
+}, \AssertionError::class /* todo: assert message */);
diff --git a/tests/fn.assertFunction.allRequired.phpt b/tests/fn.assertFunction.allRequired.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..05f7078b00c66961c1a98765bbb886999b20db33
--- /dev/null
+++ b/tests/fn.assertFunction.allRequired.phpt
@@ -0,0 +1,20 @@
+<?php declare(strict_types=1);
+namespace MyTestNamespace;
+require __DIR__ . '/bootstrap.php';
+require __DIR__ . '/testClasses.php';
+use function Grifart\AssertFunction\{assertFunction, optional, parameters};
+use Tester\Assert;
+
+
+$f1 = function(T1 $t1, T2 $t2): T3 {return new T3;};
+assertFunction($f1, parameters(T1::class, T2::class), T3::class);
+
+// Optional by accident: parameter
+Assert::exception(function () use ($f1) {
+	assertFunction($f1, parameters(optional(T1::class), T2::class), T3::class);
+}, \AssertionError::class);
+
+// Optional by accident: return type
+Assert::exception(function () use ($f1) {
+	assertFunction($f1, parameters(T1::class, T2::class), optional(T3::class));
+}, \AssertionError::class);
diff --git a/tests/fn.assertFunction.wrongNumberOfArguments.phpt b/tests/fn.assertFunction.wrongNumberOfArguments.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..198a8673d5ee705834366697f745c97f51d4ff79
--- /dev/null
+++ b/tests/fn.assertFunction.wrongNumberOfArguments.phpt
@@ -0,0 +1,18 @@
+<?php declare(strict_types=1);
+namespace MyTestNamespace;
+require __DIR__ . '/bootstrap.php';
+require __DIR__ . '/testClasses.php';
+use function Grifart\AssertFunction\{assertFunction, optional, parameters};
+use Tester\Assert;
+
+
+$f1 = function(T1 $t1, T2 $t2): T3 {return new T3;};
+
+// Wrong number of arguments
+Assert::exception(function () use ($f1) {
+	assertFunction($f1, parameters(T1::class), T3::class);
+}, \AssertionError::class);
+
+Assert::exception(function () use ($f1) {
+	assertFunction($f1, parameters(T1::class, T2::class, T1::class), T3::class);
+}, \AssertionError::class);
diff --git a/tests/php.ini b/tests/php.ini
new file mode 100644
index 0000000000000000000000000000000000000000..a7a9ac23525c4a41d72aebc3e9bab522ba78875c
--- /dev/null
+++ b/tests/php.ini
@@ -0,0 +1,7 @@
+# http://php.net/manual/en/function.assert.php
+# default
+zend.assertions = 1
+
+# required by tests
+assert.exception = 1
+
diff --git a/tests/testClasses.php b/tests/testClasses.php
new file mode 100644
index 0000000000000000000000000000000000000000..0bbb77d4acf1cb82644e99a7ea9544a4c1da800c
--- /dev/null
+++ b/tests/testClasses.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace MyTestNamespace;
+
+class T1 {};
+class T2 {};
+class T3 {};