diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0b1acda..aef96c7 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -45,6 +45,12 @@ Usages are varied, but include
 
     parser = MyParser()
 
+Changes:
+
+* Registered `error_handler` callbacks are required to raise an exception.
+  If a handler is invoked and no exception is raised, `webargs` will raise
+  a `ValueError` (:issue:`527`)
+
 6.1.1 (2020-09-08)
 ******************
 
diff --git a/src/webargs/core.py b/src/webargs/core.py
index 922bd95..bab7f8b 100644
--- a/src/webargs/core.py
+++ b/src/webargs/core.py
@@ -283,10 +283,9 @@ class Parser:
                 error_status_code=error_status_code,
                 error_headers=error_headers,
             )
-            warnings.warn(
-                "_on_validation_error hook did not raise an exception and flow "
-                "of control returned to parse(). You may get unexpected results"
-            )
+            raise ValueError(
+                "_on_validation_error hook did not raise an exception"
+            ) from error
         return data
 
     def get_default_request(self):

diff --git a/tests/test_core.py b/tests/test_core.py
index 1995636..293d108 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -289,14 +289,19 @@ def test_value_error_raised_if_parse_called_with_invalid_location(parser, web_re
 
 @mock.patch("webargs.core.Parser.handle_error")
 def test_handle_error_called_when_parsing_raises_error(handle_error, web_request):
+    # handle_error must raise an error to be valid
+    handle_error.side_effect = ValidationError("parsing failed")
+
     def always_fail(*args, **kwargs):
         raise ValidationError("error occurred")
 
     p = Parser()
     assert handle_error.call_count == 0
-    p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
+    with pytest.raises(ValidationError):
+        p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
     assert handle_error.call_count == 1
-    p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
+    with pytest.raises(ValidationError):
+        p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
     assert handle_error.call_count == 2
 
 
@@ -360,6 +365,25 @@ def test_custom_error_handler_decorator(web_request):
         parser.parse(mock_schema, web_request)
 
 
+def test_custom_error_handler_must_reraise(web_request):
+    class CustomError(Exception):
+        pass
+
+    mock_schema = mock.Mock(spec=Schema)
+    mock_schema.strict = True
+    mock_schema.load.side_effect = ValidationError("parsing json failed")
+    parser = Parser()
+
+    @parser.error_handler
+    def handle_error(error, req, schema, *, error_status_code, error_headers):
+        pass
+
+    # because the handler above does not raise a new error, the parser should
+    # raise a ValueError -- indicating a programming error
+    with pytest.raises(ValueError):
+        parser.parse(mock_schema, web_request)
+
+
 def test_custom_location_loader(web_request):
     web_request.data = {"foo": 42}
 
diff --git a/tests/test_flaskparser.py b/tests/test_flaskparser.py
index 3429992..e9f08a4 100644
--- a/tests/test_flaskparser.py
+++ b/tests/test_flaskparser.py
@@ -1,4 +1,4 @@
-from werkzeug.exceptions import HTTPException
+from werkzeug.exceptions import HTTPException, BadRequest
 import pytest
 
 from flask import Flask
@@ -85,6 +85,9 @@ class TestFlaskParser(CommonTestCase):
 
 @mock.patch("webargs.flaskparser.abort")
 def test_abort_called_on_validation_error(mock_abort):
+    # error handling must raise an error to be valid
+    mock_abort.side_effect = BadRequest("foo")
+
     app = Flask("testapp")
 
     def validate(x):
@@ -97,7 +100,8 @@ def test_abort_called_on_validation_error(mock_abort):
         data=json.dumps({"value": 41}),
         content_type="application/json",
     ):
-        parser.parse(argmap)
+        with pytest.raises(HTTPException):
+            parser.parse(argmap)
     mock_abort.assert_called()
     abort_args, abort_kwargs = mock_abort.call_args
     assert abort_args[0] == 422

Failing to (re)raise an error when handling validation errors should raise a new error
Per #525 , we're going to start warning if you setup an error handler which does not, itself, raise an error. The result of failing to raise in your handler is that parsing "falls through" and returns incorrect data (`None` today, but the behavior is not well-defined) to the caller.

In version 7.0 of webargs, we should change this to raise an error, because it's not correct usage of webargs.

I think the most developer-friendly thing for us to do in this case is to raise `ValueError` or something similar (i.e. not a `ValidationError`, which they might be catching and handling outside of the parsing context). That will make it immediately apparent to the developer that they've done something wrong.
We can't make any guesses about whether or not people are testing their error handlers well, so no matter what we do here, there's a risk that people's applications crashfail in these scenarios.
tests/test_core.py::test_custom_error_handler_must_reraise
tests/test_core.py::test_load_json_called_by_parse_default tests/test_core.py::test_load_nondefault_called_by_parse_with_location[querystring] tests/test_core.py::test_load_nondefault_called_by_parse_with_location[form] tests/test_core.py::test_load_nondefault_called_by_parse_with_location[headers] tests/test_core.py::test_load_nondefault_called_by_parse_with_location[cookies] tests/test_core.py::test_load_nondefault_called_by_parse_with_location[files] tests/test_core.py::test_parse tests/test_core.py::test_parse_with_unknown_behavior_specified[schema_instance] tests/test_core.py::test_parse_with_unknown_behavior_specified[parse_call] tests/test_core.py::test_parse_with_unknown_behavior_specified[parser_default] tests/test_core.py::test_parse_with_unknown_behavior_specified[parser_class_default] tests/test_core.py::test_parse_with_explicit_unknown_overrides_schema tests/test_core.py::test_parse_required_arg_raises_validation_error tests/test_core.py::test_arg_not_required_excluded_in_parsed_output tests/test_core.py::test_arg_allow_none tests/test_core.py::test_parse_required_arg tests/test_core.py::test_parse_required_list tests/test_core.py::test_parse_list_allow_none tests/test_core.py::test_parse_list_dont_allow_none tests/test_core.py::test_parse_empty_list tests/test_core.py::test_parse_missing_list tests/test_core.py::test_default_location tests/test_core.py::test_missing_with_default tests/test_core.py::test_default_can_be_none tests/test_core.py::test_arg_with_default_and_location tests/test_core.py::test_value_error_raised_if_parse_called_with_invalid_location tests/test_core.py::test_handle_error_called_when_parsing_raises_error tests/test_core.py::test_handle_error_reraises_errors tests/test_core.py::test_location_as_init_argument tests/test_core.py::test_custom_error_handler tests/test_core.py::test_custom_error_handler_decorator tests/test_core.py::test_custom_location_loader tests/test_core.py::test_custom_location_loader_with_data_key tests/test_core.py::test_full_input_validation tests/test_core.py::test_full_input_validation_with_multiple_validators tests/test_core.py::test_required_with_custom_error tests/test_core.py::test_required_with_custom_error_and_validation_error tests/test_core.py::test_full_input_validator_receives_nonascii_input tests/test_core.py::test_invalid_argument_for_validate tests/test_core.py::test_multidict_proxy[input_dict0] tests/test_core.py::test_multidict_proxy[input_dict1] tests/test_core.py::test_multidict_proxy[input_dict2] tests/test_core.py::test_parse_with_data_key tests/test_core.py::test_parse_with_data_key_retains_field_name_in_error tests/test_core.py::test_parse_nested_with_data_key tests/test_core.py::test_parse_nested_with_missing_key_and_data_key tests/test_core.py::test_parse_nested_with_default tests/test_core.py::test_nested_many tests/test_core.py::test_use_args tests/test_core.py::test_use_args_stacked tests/test_core.py::test_use_kwargs_stacked tests/test_core.py::test_decorators_dont_change_docstring[use_args] tests/test_core.py::test_decorators_dont_change_docstring[use_kwargs] tests/test_core.py::test_list_allowed_missing tests/test_core.py::test_int_list_allowed_missing tests/test_core.py::test_multiple_arg_required_with_int_conversion tests/test_core.py::test_parse_with_callable tests/test_core.py::test_use_args_callable tests/test_core.py::TestPassingSchema::test_passing_schema_to_parse tests/test_core.py::TestPassingSchema::test_use_args_can_be_passed_a_schema tests/test_core.py::TestPassingSchema::test_passing_schema_factory_to_parse tests/test_core.py::TestPassingSchema::test_use_args_can_be_passed_a_schema_factory tests/test_core.py::TestPassingSchema::test_use_kwargs_can_be_passed_a_schema tests/test_core.py::TestPassingSchema::test_use_kwargs_can_be_passed_a_schema_factory tests/test_core.py::TestPassingSchema::test_use_kwargs_stacked tests/test_core.py::TestPassingSchema::test_parse_does_not_add_missing_values_to_schema_validator tests/test_core.py::test_use_args_with_custom_location_in_parser tests/test_core.py::test_use_kwargs tests/test_core.py::test_use_kwargs_with_arg_missing tests/test_core.py::test_delimited_list_default_delimiter tests/test_core.py::test_delimited_tuple_default_delimiter tests/test_core.py::test_delimited_tuple_incorrect_arity tests/test_core.py::test_delimited_list_with_datetime tests/test_core.py::test_delimited_list_custom_delimiter tests/test_core.py::test_delimited_tuple_custom_delimiter tests/test_core.py::test_delimited_list_load_list_errors tests/test_core.py::test_delimited_tuple_load_list_errors tests/test_core.py::test_delimited_list_passed_invalid_type tests/test_core.py::test_delimited_tuple_passed_invalid_type tests/test_core.py::test_missing_list_argument_not_in_parsed_result tests/test_core.py::test_type_conversion_with_multiple_required tests/test_core.py::test_validation_errors_in_validator_are_passed_to_handle_error tests/test_core.py::test_parse_basic tests/test_core.py::test_parse_raises_validation_error_if_data_invalid tests/test_core.py::test_dict2schema tests/test_core.py::test_dict2schema_doesnt_add_to_class_registry tests/test_core.py::test_dict2schema_with_nesting tests/test_core.py::test_is_json tests/test_core.py::test_get_mimetype tests/test_core.py::test_parse_with_error_status_code_and_headers tests/test_core.py::test_custom_schema_class tests/test_core.py::test_custom_default_schema_class tests/test_flaskparser.py::TestFlaskParser::test_parse_querystring_args tests/test_flaskparser.py::TestFlaskParser::test_parse_form tests/test_flaskparser.py::TestFlaskParser::test_parse_json tests/test_flaskparser.py::TestFlaskParser::test_parse_json_missing tests/test_flaskparser.py::TestFlaskParser::test_parse_json_or_form tests/test_flaskparser.py::TestFlaskParser::test_parse_querystring_default tests/test_flaskparser.py::TestFlaskParser::test_parse_json_default tests/test_flaskparser.py::TestFlaskParser::test_parse_json_with_charset tests/test_flaskparser.py::TestFlaskParser::test_parse_json_with_vendor_media_type tests/test_flaskparser.py::TestFlaskParser::test_parse_ignore_extra_data tests/test_flaskparser.py::TestFlaskParser::test_parse_json_empty tests/test_flaskparser.py::TestFlaskParser::test_parse_json_error_unexpected_int tests/test_flaskparser.py::TestFlaskParser::test_parse_json_error_unexpected_list tests/test_flaskparser.py::TestFlaskParser::test_parse_json_many_schema_invalid_input tests/test_flaskparser.py::TestFlaskParser::test_parse_json_many_schema tests/test_flaskparser.py::TestFlaskParser::test_parse_json_many_schema_error_malformed_data tests/test_flaskparser.py::TestFlaskParser::test_parsing_form_default tests/test_flaskparser.py::TestFlaskParser::test_parse_querystring_multiple tests/test_flaskparser.py::TestFlaskParser::test_parse_querystring_multiple_single_value tests/test_flaskparser.py::TestFlaskParser::test_parse_form_multiple tests/test_flaskparser.py::TestFlaskParser::test_parse_json_list tests/test_flaskparser.py::TestFlaskParser::test_parse_json_list_error_malformed_data tests/test_flaskparser.py::TestFlaskParser::test_parse_json_with_nonascii_chars tests/test_flaskparser.py::TestFlaskParser::test_parse_json_with_nonutf8_chars tests/test_flaskparser.py::TestFlaskParser::test_validation_error_returns_422_response tests/test_flaskparser.py::TestFlaskParser::test_user_validation_error_returns_422_response_by_default tests/test_flaskparser.py::TestFlaskParser::test_use_args_decorator tests/test_flaskparser.py::TestFlaskParser::test_use_args_with_path_param tests/test_flaskparser.py::TestFlaskParser::test_use_args_with_validation tests/test_flaskparser.py::TestFlaskParser::test_use_kwargs_decorator tests/test_flaskparser.py::TestFlaskParser::test_use_kwargs_with_path_param tests/test_flaskparser.py::TestFlaskParser::test_parsing_headers tests/test_flaskparser.py::TestFlaskParser::test_parsing_cookies tests/test_flaskparser.py::TestFlaskParser::test_parse_nested_json tests/test_flaskparser.py::TestFlaskParser::test_parse_nested_many_json tests/test_flaskparser.py::TestFlaskParser::test_parse_nested_many_missing tests/test_flaskparser.py::TestFlaskParser::test_parse_files tests/test_flaskparser.py::TestFlaskParser::test_empty_json tests/test_flaskparser.py::TestFlaskParser::test_empty_json_with_headers tests/test_flaskparser.py::TestFlaskParser::test_invalid_json tests/test_flaskparser.py::TestFlaskParser::test_content_type_mismatch[/echo_json-{"name": tests/test_flaskparser.py::TestFlaskParser::test_content_type_mismatch[/echo_form-payload1-application/json] tests/test_flaskparser.py::TestFlaskParser::test_parsing_view_args tests/test_flaskparser.py::TestFlaskParser::test_parsing_invalid_view_arg tests/test_flaskparser.py::TestFlaskParser::test_use_args_with_view_args_parsing tests/test_flaskparser.py::TestFlaskParser::test_use_args_on_a_method_view tests/test_flaskparser.py::TestFlaskParser::test_use_kwargs_on_a_method_view tests/test_flaskparser.py::TestFlaskParser::test_use_kwargs_with_missing_data tests/test_flaskparser.py::TestFlaskParser::test_nested_many_with_data_key tests/test_flaskparser.py::TestFlaskParser::test_parsing_unexpected_headers_when_raising tests/test_flaskparser.py::test_abort_called_on_validation_error tests/test_flaskparser.py::test_load_json_returns_missing_if_no_data[None] tests/test_flaskparser.py::test_load_json_returns_missing_if_no_data[application/json] tests/test_flaskparser.py::test_abort_with_message tests/test_flaskparser.py::test_abort_has_serializable_data