Repository: strawberry-graphql/strawberry

Issue:
StrawberryGraphQLError in Custom Extension causes 500 error response instead of formatted error
## Description
I am attempting to add a Custom Extension to catch queries that are too large. I then want the extension to raise StrawberryGraphQLError and return a properly formatted error message. 

The issue is that when I raise StrawberryGraphQLError an internal error response is sent to the requester.

```
MAX_QUERY_LENGTH = 8192 # 8KB

class MaxQueryLengthExtension(SchemaExtension):
    async def on_operation(self):
        if len(self.execution_context.query) > MAX_QUERY_LENGTH:
            raise StrawberryGraphQLError(message="Query too large")
        yield
```

Stack Trace

```
2023-11-06 11:26:31 Exception in ASGI application
2023-11-06 11:26:31 Traceback (most recent call last):
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/uvicorn/protocols/http/httptools_impl.py", line 426, in run_asgi
2023-11-06 11:26:31     result = await app(  # type: ignore[func-returns-value]
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
2023-11-06 11:26:31     return await self.app(scope, receive, send)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/fastapi/applications.py", line 292, in __call__
2023-11-06 11:26:31     await super().__call__(scope, receive, send)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/starlette/applications.py", line 122, in __call__
2023-11-06 11:26:31     await self.middleware_stack(scope, receive, send)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/ddtrace/contrib/asgi/middleware.py", line 275, in __call__
2023-11-06 11:26:31     reraise(exc_type, exc_val, exc_tb)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/six.py", line 719, in reraise
2023-11-06 11:26:31     raise value
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/ddtrace/contrib/asgi/middleware.py", line 270, in __call__
2023-11-06 11:26:31     return await self.app(scope, receive, wrapped_send)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in __call__
2023-11-06 11:26:31     raise exc
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in __call__
2023-11-06 11:26:31     await self.app(scope, receive, _send)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/cors.py", line 83, in __call__
2023-11-06 11:26:31     await self.app(scope, receive, send)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
2023-11-06 11:26:31     raise exc
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
2023-11-06 11:26:31     await self.app(scope, receive, sender)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__
2023-11-06 11:26:31     raise e
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__
2023-11-06 11:26:31     await self.app(scope, receive, send)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 718, in __call__
2023-11-06 11:26:31     await route.handle(scope, receive, send)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 276, in handle
2023-11-06 11:26:31     await self.app(scope, receive, send)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 66, in app
2023-11-06 11:26:31     response = await func(request)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/fastapi/routing.py", line 273, in app
2023-11-06 11:26:31     raw_response = await run_endpoint_function(
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/fastapi/routing.py", line 190, in run_endpoint_function
2023-11-06 11:26:31     return await dependant.call(**values)
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/strawberry/fastapi/router.py", line 227, in handle_http_post
2023-11-06 11:26:31     return await self.run(
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/strawberry/http/async_base_view.py", line 176, in run
2023-11-06 11:26:31     result = await self.execute_operation(
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/strawberry/http/async_base_view.py", line 115, in execute_operation
2023-11-06 11:26:31     return await self.schema.execute(
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/strawberry/schema/schema.py", line 256, in execute
2023-11-06 11:26:31     result = await execute(
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/strawberry/schema/execute.py", line 86, in execute
2023-11-06 11:26:31     async with extensions_runner.operation():
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/strawberry/extensions/context.py", line 191, in __aenter__
2023-11-06 11:26:31     await self.run_hooks_async()
2023-11-06 11:26:31   File "/usr/local/lib/python3.9/site-packages/strawberry/extensions/context.py", line 175, in run_hooks_async
2023-11-06 11:26:31     await hook.initialized_hook.__anext__()  # type: ignore[union-attr]
2023-11-06 11:26:31   File "/src/src/routers/graphql.py", line 78, in on_operation
2023-11-06 11:26:31     raise StrawberryGraphQLError(message="Query too large")
2023-11-06 11:26:31 strawberry.exceptions.StrawberryGraphQLError: Query too large
```


## Describe the Bug



## System Information

 - Operating system: Mac with Docker containers
 - Strawberry version (if applicable): `strawberry-graphql[fastapi] == 0.209.2`


<!-- POLAR PLEDGE BADGE START -->
## Upvote & Fund

- We're using [Polar.sh](https://polar.sh/strawberry-graphql) so you can upvote and help fund this issue.
- We receive the funding once the issue is completed & confirmed by you.
- Thank you in advance for helping prioritize & fund our backlog.

<a href="https://polar.sh/strawberry-graphql/strawberry/issues/3204">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/strawberry-graphql/strawberry/issues/3204/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/strawberry-graphql/strawberry/issues/3204/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:

Resolving patch:
diff --git a/strawberry/schema/execute.py b/strawberry/schema/execute.py
--- a/strawberry/schema/execute.py
+++ b/strawberry/schema/execute.py
@@ -88,77 +88,82 @@ async def execute(
         extensions=list(extensions),
     )
 
-    async with extensions_runner.operation():
-        # Note: In graphql-core the schema would be validated here but in
-        # Strawberry we are validating it at initialisation time instead
-        if not execution_context.query:
-            raise MissingQueryError()
-
-        async with extensions_runner.parsing():
-            try:
-                if not execution_context.graphql_document:
-                    execution_context.graphql_document = parse_document(
-                        execution_context.query, **execution_context.parse_options
+    try:
+        async with extensions_runner.operation():
+            # Note: In graphql-core the schema would be validated here but in
+            # Strawberry we are validating it at initialisation time instead
+            if not execution_context.query:
+                raise MissingQueryError()
+
+            async with extensions_runner.parsing():
+                try:
+                    if not execution_context.graphql_document:
+                        execution_context.graphql_document = parse_document(
+                            execution_context.query, **execution_context.parse_options
+                        )
+
+                except GraphQLError as exc:
+                    execution_context.errors = [exc]
+                    process_errors([exc], execution_context)
+                    return ExecutionResult(
+                        data=None,
+                        errors=[exc],
+                        extensions=await extensions_runner.get_extensions_results(),
                     )
 
-            except GraphQLError as error:
-                execution_context.errors = [error]
-                process_errors([error], execution_context)
-                return ExecutionResult(
-                    data=None,
-                    errors=[error],
-                    extensions=await extensions_runner.get_extensions_results(),
-                )
-
-            except Exception as error:  # pragma: no cover
-                error = GraphQLError(str(error), original_error=error)
-
-                execution_context.errors = [error]
-                process_errors([error], execution_context)
-
-                return ExecutionResult(
-                    data=None,
-                    errors=[error],
-                    extensions=await extensions_runner.get_extensions_results(),
-                )
-
-        if execution_context.operation_type not in allowed_operation_types:
-            raise InvalidOperationTypeError(execution_context.operation_type)
-
-        async with extensions_runner.validation():
-            _run_validation(execution_context)
-            if execution_context.errors:
-                process_errors(execution_context.errors, execution_context)
-                return ExecutionResult(data=None, errors=execution_context.errors)
-
-        async with extensions_runner.executing():
-            if not execution_context.result:
-                result = original_execute(
-                    schema,
-                    execution_context.graphql_document,
-                    root_value=execution_context.root_value,
-                    middleware=extensions_runner.as_middleware_manager(),
-                    variable_values=execution_context.variables,
-                    operation_name=execution_context.operation_name,
-                    context_value=execution_context.context,
-                    execution_context_class=execution_context_class,
-                )
-
-                if isawaitable(result):
-                    result = await cast(Awaitable["GraphQLExecutionResult"], result)
-
-                result = cast("GraphQLExecutionResult", result)
-                execution_context.result = result
-                # Also set errors on the execution_context so that it's easier
-                # to access in extensions
-                if result.errors:
-                    execution_context.errors = result.errors
-
-                    # Run the `Schema.process_errors` function here before
-                    # extensions have a chance to modify them (see the MaskErrors
-                    # extension). That way we can log the original errors but
-                    # only return a sanitised version to the client.
-                    process_errors(result.errors, execution_context)
+            if execution_context.operation_type not in allowed_operation_types:
+                raise InvalidOperationTypeError(execution_context.operation_type)
+
+            async with extensions_runner.validation():
+                _run_validation(execution_context)
+                if execution_context.errors:
+                    process_errors(execution_context.errors, execution_context)
+                    return ExecutionResult(data=None, errors=execution_context.errors)
+
+            async with extensions_runner.executing():
+                if not execution_context.result:
+                    result = original_execute(
+                        schema,
+                        execution_context.graphql_document,
+                        root_value=execution_context.root_value,
+                        middleware=extensions_runner.as_middleware_manager(),
+                        variable_values=execution_context.variables,
+                        operation_name=execution_context.operation_name,
+                        context_value=execution_context.context,
+                        execution_context_class=execution_context_class,
+                    )
+
+                    if isawaitable(result):
+                        result = await cast(Awaitable["GraphQLExecutionResult"], result)
+
+                    result = cast("GraphQLExecutionResult", result)
+                    execution_context.result = result
+                    # Also set errors on the execution_context so that it's easier
+                    # to access in extensions
+                    if result.errors:
+                        execution_context.errors = result.errors
+
+                        # Run the `Schema.process_errors` function here before
+                        # extensions have a chance to modify them (see the MaskErrors
+                        # extension). That way we can log the original errors but
+                        # only return a sanitised version to the client.
+                        process_errors(result.errors, execution_context)
+
+    except (MissingQueryError, InvalidOperationTypeError) as e:
+        raise e
+    except Exception as exc:
+        error = (
+            exc
+            if isinstance(exc, GraphQLError)
+            else GraphQLError(str(exc), original_error=exc)
+        )
+        execution_context.errors = [error]
+        process_errors([error], execution_context)
+        return ExecutionResult(
+            data=None,
+            errors=[error],
+            extensions=await extensions_runner.get_extensions_results(),
+        )
 
     return ExecutionResult(
         data=execution_context.result.data,
@@ -181,80 +186,86 @@ def execute_sync(
         extensions=list(extensions),
     )
 
-    with extensions_runner.operation():
-        # Note: In graphql-core the schema would be validated here but in
-        # Strawberry we are validating it at initialisation time instead
-        if not execution_context.query:
-            raise MissingQueryError()
-
-        with extensions_runner.parsing():
-            try:
-                if not execution_context.graphql_document:
-                    execution_context.graphql_document = parse_document(
-                        execution_context.query, **execution_context.parse_options
+    try:
+        with extensions_runner.operation():
+            # Note: In graphql-core the schema would be validated here but in
+            # Strawberry we are validating it at initialisation time instead
+            if not execution_context.query:
+                raise MissingQueryError()
+
+            with extensions_runner.parsing():
+                try:
+                    if not execution_context.graphql_document:
+                        execution_context.graphql_document = parse_document(
+                            execution_context.query, **execution_context.parse_options
+                        )
+
+                except GraphQLError as exc:
+                    execution_context.errors = [exc]
+                    process_errors([exc], execution_context)
+                    return ExecutionResult(
+                        data=None,
+                        errors=[exc],
+                        extensions=extensions_runner.get_extensions_results_sync(),
                     )
 
-            except GraphQLError as error:
-                execution_context.errors = [error]
-                process_errors([error], execution_context)
-                return ExecutionResult(
-                    data=None,
-                    errors=[error],
-                    extensions=extensions_runner.get_extensions_results_sync(),
-                )
-
-            except Exception as error:  # pragma: no cover
-                error = GraphQLError(str(error), original_error=error)
-
-                execution_context.errors = [error]
-                process_errors([error], execution_context)
-                return ExecutionResult(
-                    data=None,
-                    errors=[error],
-                    extensions=extensions_runner.get_extensions_results_sync(),
-                )
-
-        if execution_context.operation_type not in allowed_operation_types:
-            raise InvalidOperationTypeError(execution_context.operation_type)
-
-        with extensions_runner.validation():
-            _run_validation(execution_context)
-            if execution_context.errors:
-                process_errors(execution_context.errors, execution_context)
-                return ExecutionResult(data=None, errors=execution_context.errors)
-
-        with extensions_runner.executing():
-            if not execution_context.result:
-                result = original_execute(
-                    schema,
-                    execution_context.graphql_document,
-                    root_value=execution_context.root_value,
-                    middleware=extensions_runner.as_middleware_manager(),
-                    variable_values=execution_context.variables,
-                    operation_name=execution_context.operation_name,
-                    context_value=execution_context.context,
-                    execution_context_class=execution_context_class,
-                )
-
-                if isawaitable(result):
-                    result = cast(Awaitable["GraphQLExecutionResult"], result)
-                    ensure_future(result).cancel()
-                    raise RuntimeError(
-                        "GraphQL execution failed to complete synchronously."
+            if execution_context.operation_type not in allowed_operation_types:
+                raise InvalidOperationTypeError(execution_context.operation_type)
+
+            with extensions_runner.validation():
+                _run_validation(execution_context)
+                if execution_context.errors:
+                    process_errors(execution_context.errors, execution_context)
+                    return ExecutionResult(data=None, errors=execution_context.errors)
+
+            with extensions_runner.executing():
+                if not execution_context.result:
+                    result = original_execute(
+                        schema,
+                        execution_context.graphql_document,
+                        root_value=execution_context.root_value,
+                        middleware=extensions_runner.as_middleware_manager(),
+                        variable_values=execution_context.variables,
+                        operation_name=execution_context.operation_name,
+                        context_value=execution_context.context,
+                        execution_context_class=execution_context_class,
                     )
 
-                result = cast("GraphQLExecutionResult", result)
-                execution_context.result = result
-                # Also set errors on the execution_context so that it's easier
-                # to access in extensions
-                if result.errors:
-                    execution_context.errors = result.errors
-
-                    # Run the `Schema.process_errors` function here before
-                    # extensions have a chance to modify them (see the MaskErrors
-                    # extension). That way we can log the original errors but
-                    # only return a sanitised version to the client.
-                    process_errors(result.errors, execution_context)
+                    if isawaitable(result):
+                        result = cast(Awaitable["GraphQLExecutionResult"], result)
+                        ensure_future(result).cancel()
+                        raise RuntimeError(
+                            "GraphQL execution failed to complete synchronously."
+                        )
+
+                    result = cast("GraphQLExecutionResult", result)
+                    execution_context.result = result
+                    # Also set errors on the execution_context so that it's easier
+                    # to access in extensions
+                    if result.errors:
+                        execution_context.errors = result.errors
+
+                        # Run the `Schema.process_errors` function here before
+                        # extensions have a chance to modify them (see the MaskErrors
+                        # extension). That way we can log the original errors but
+                        # only return a sanitised version to the client.
+                        process_errors(result.errors, execution_context)
+
+    except (MissingQueryError, InvalidOperationTypeError) as e:
+        raise e
+    except Exception as exc:
+        error = (
+            exc
+            if isinstance(exc, GraphQLError)
+            else GraphQLError(str(exc), original_error=exc)
+        )
+        execution_context.errors = [error]
+        process_errors([error], execution_context)
+        return ExecutionResult(
+            data=None,
+            errors=[error],
+            extensions=extensions_runner.get_extensions_results_sync(),
+        )
 
     return ExecutionResult(
         data=execution_context.result.data,


Test patch:
diff --git a/tests/schema/extensions/test_extensions.py b/tests/schema/extensions/test_extensions.py
--- a/tests/schema/extensions/test_extensions.py
+++ b/tests/schema/extensions/test_extensions.py
@@ -467,8 +467,10 @@ def on_executing_start(self):
     schema = strawberry.Schema(
         query=default_query_types_and_query.query_type, extensions=[WrongUsageExtension]
     )
-    with pytest.raises(ValueError):
-        schema.execute_sync(default_query_types_and_query.query)
+
+    result = schema.execute_sync(default_query_types_and_query.query)
+    assert len(result.errors) == 1
+    assert isinstance(result.errors[0].original_error, ValueError)
 
 
 async def test_legacy_extension_supported():
@@ -628,8 +630,184 @@ def string(self) -> str:
         schema.execute_sync(query)
 
 
+class ExceptionTestingExtension(SchemaExtension):
+    def __init__(self, failing_hook: str):
+        self.failing_hook = failing_hook
+        self.called_hooks = set()
+
+    def on_operation(self):
+        if self.failing_hook == "on_operation_start":
+            raise Exception(self.failing_hook)
+        self.called_hooks.add(1)
+
+        with contextlib.suppress(Exception):
+            yield
+
+        if self.failing_hook == "on_operation_end":
+            raise Exception(self.failing_hook)
+        self.called_hooks.add(8)
+
+    def on_parse(self):
+        if self.failing_hook == "on_parse_start":
+            raise Exception(self.failing_hook)
+        self.called_hooks.add(2)
+
+        with contextlib.suppress(Exception):
+            yield
+
+        if self.failing_hook == "on_parse_end":
+            raise Exception(self.failing_hook)
+        self.called_hooks.add(3)
+
+    def on_validate(self):
+        if self.failing_hook == "on_validate_start":
+            raise Exception(self.failing_hook)
+        self.called_hooks.add(4)
+
+        with contextlib.suppress(Exception):
+            yield
+
+        if self.failing_hook == "on_validate_end":
+            raise Exception(self.failing_hook)
+        self.called_hooks.add(5)
+
+    def on_execute(self):
+        if self.failing_hook == "on_execute_start":
+            raise Exception(self.failing_hook)
+        self.called_hooks.add(6)
+
+        with contextlib.suppress(Exception):
+            yield
+
+        if self.failing_hook == "on_execute_end":
+            raise Exception(self.failing_hook)
+        self.called_hooks.add(7)
+
+
+@pytest.mark.parametrize(
+    "failing_hook",
+    (
+        "on_operation_start",
+        "on_operation_end",
+        "on_parse_start",
+        "on_parse_end",
+        "on_validate_start",
+        "on_validate_end",
+        "on_execute_start",
+        "on_execute_end",
+    ),
+)
+@pytest.mark.asyncio
+async def test_exceptions_are_included_in_the_execution_result(failing_hook):
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def ping(self) -> str:
+            return "pong"
+
+    schema = strawberry.Schema(
+        query=Query,
+        extensions=[ExceptionTestingExtension(failing_hook)],
+    )
+    document = "query { ping }"
+
+    sync_result = schema.execute_sync(document)
+    assert sync_result.errors is not None
+    assert len(sync_result.errors) == 1
+    assert sync_result.errors[0].message == failing_hook
+
+    async_result = await schema.execute(document)
+    assert async_result.errors is not None
+    assert len(async_result.errors) == 1
+    assert sync_result.errors[0].message == failing_hook
+
+
+@pytest.mark.parametrize(
+    ("failing_hook", "expected_hooks"),
+    (
+        ("on_operation_start", set()),
+        ("on_parse_start", {1, 8}),
+        ("on_parse_end", {1, 2, 8}),
+        ("on_validate_start", {1, 2, 3, 8}),
+        ("on_validate_end", {1, 2, 3, 4, 8}),
+        ("on_execute_start", {1, 2, 3, 4, 5, 8}),
+        ("on_execute_end", {1, 2, 3, 4, 5, 6, 8}),
+        ("on_operation_end", {1, 2, 3, 4, 5, 6, 7}),
+    ),
+)
+@pytest.mark.asyncio
+async def test_exceptions_abort_evaluation(failing_hook, expected_hooks):
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def ping(self) -> str:
+            return "pong"
+
+    extension = ExceptionTestingExtension(failing_hook)
+    schema = strawberry.Schema(query=Query, extensions=[extension])
+    document = "query { ping }"
+
+    extension.called_hooks = set()
+    schema.execute_sync(document)
+    assert extension.called_hooks == expected_hooks
+
+    extension.called_hooks = set()
+    await schema.execute(document)
+    assert extension.called_hooks == expected_hooks
+
+
+async def test_generic_exceptions_get_wrapped_in_a_graphql_error():
+    exception = Exception("This should be wrapped in a GraphQL error")
+
+    class MyExtension(SchemaExtension):
+        def on_parse(self):
+            raise exception
+
+    @strawberry.type
+    class Query:
+        ping: str = "pong"
+
+    schema = strawberry.Schema(query=Query, extensions=[MyExtension])
+    query = "query { ping }"
+
+    sync_result = schema.execute_sync(query)
+    assert len(sync_result.errors) == 1
+    assert isinstance(sync_result.errors[0], GraphQLError)
+    assert sync_result.errors[0].original_error == exception
+
+    async_result = await schema.execute(query)
+    assert len(async_result.errors) == 1
+    assert isinstance(async_result.errors[0], GraphQLError)
+    assert async_result.errors[0].original_error == exception
+
+
+async def test_graphql_errors_get_not_wrapped_in_a_graphql_error():
+    exception = GraphQLError("This should not be wrapped in a GraphQL error")
+
+    class MyExtension(SchemaExtension):
+        def on_parse(self):
+            raise exception
+
+    @strawberry.type
+    class Query:
+        ping: str = "pong"
+
+    schema = strawberry.Schema(query=Query, extensions=[MyExtension])
+    query = "query { ping }"
+
+    sync_result = schema.execute_sync(query)
+    assert len(sync_result.errors) == 1
+    assert sync_result.errors[0] == exception
+    assert sync_result.errors[0].original_error is None
+
+    async_result = await schema.execute(query)
+    assert len(async_result.errors) == 1
+    assert async_result.errors[0] == exception
+    assert async_result.errors[0].original_error is None
+
+
 @pytest.mark.asyncio
-async def test_dont_swallow_errors_in_parsing_hooks():
+async def test_non_parsing_errors_are_not_swallowed_by_parsing_hooks():
     class MyExtension(SchemaExtension):
         def on_parse(self):
             raise Exception("This shouldn't be swallowed")
@@ -643,14 +821,16 @@ def ping(self) -> str:
     schema = strawberry.Schema(query=Query, extensions=[MyExtension])
     query = "query { string }"
 
-    with pytest.raises(Exception, match="This shouldn't be swallowed"):
-        schema.execute_sync(query)
+    sync_result = schema.execute_sync(query)
+    assert len(sync_result.errors) == 1
+    assert sync_result.errors[0].message == "This shouldn't be swallowed"
 
-    with pytest.raises(Exception, match="This shouldn't be swallowed"):
-        await schema.execute(query)
+    async_result = await schema.execute(query)
+    assert len(async_result.errors) == 1
+    assert async_result.errors[0].message == "This shouldn't be swallowed"
 
 
-def test_on_parsing_end_called_when_errors():
+def test_on_parsing_end_is_called_with_parsing_errors():
     execution_errors = False
 
     class MyExtension(SchemaExtension):
@@ -696,7 +876,8 @@ def on_execute(self):
     class Query:
         food: str = "strawberry"
 
-    schema = strawberry.Schema(query=Query, extensions=[ExtensionB, ExtensionC])
+    extensions = [ExtensionB, ExtensionC]
+    schema = strawberry.Schema(query=Query, extensions=extensions)
 
     query = """
         query TestQuery {
@@ -722,8 +903,9 @@ class Query:
 
     schema = strawberry.Schema(query=Query, extensions=[ExtensionA])
 
-    with pytest.raises(RuntimeError, match="failed to complete synchronously"):
-        schema.execute_sync("query { food }")
+    result = schema.execute_sync("query { food }")
+    assert len(result.errors) == 1
+    assert result.errors[0].message.endswith("failed to complete synchronously.")
 
 
 def test_extension_override_execution():
@@ -1021,24 +1203,8 @@ def hi(self) -> str:
     # Query not set on input
     query = "{ hi }"
 
-    with pytest.raises(
-        ValueError, match="Hook on_operation on <(.*)> must be callable, received 'ABC'"
-    ):
-        schema.execute_sync(query)
-
-
-@pytest.mark.asyncio
-async def test_calls_hooks_when_there_are_errors(async_extension):
-    @strawberry.type
-    class Query:
-        @strawberry.field
-        def hi(self) -> str:
-            raise Exception("This is an error")
-
-    schema = strawberry.Schema(query=Query, extensions=[async_extension])
-
-    query = "{ hi }"
-
-    result = await schema.execute(query)
-    assert result.errors
-    async_extension.perform_test()
+    result = schema.execute_sync(query)
+    assert len(result.errors) == 1
+    assert isinstance(result.errors[0].original_error, ValueError)
+    assert result.errors[0].message.startswith("Hook on_operation on <")
+    assert result.errors[0].message.endswith("> must be callable, received 'ABC'")
diff --git a/tests/schema/extensions/test_parser_cache.py b/tests/schema/extensions/test_parser_cache.py
--- a/tests/schema/extensions/test_parser_cache.py
+++ b/tests/schema/extensions/test_parser_cache.py
@@ -1,7 +1,7 @@
 from unittest.mock import patch
 
 import pytest
-from graphql import parse
+from graphql import SourceLocation, parse
 
 import strawberry
 from strawberry.extensions import MaxTokensLimiter, ParserCache
@@ -75,6 +75,26 @@ def ping(self) -> str:
     mock_parse.assert_called_with("query { hello }", max_tokens=20)
 
 
+@patch("strawberry.schema.execute.parse", wraps=parse)
+def test_parser_cache_extension_syntax_error(mock_parse):
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def hello(self) -> str:  # pragma: no cover
+            return "world"
+
+    schema = strawberry.Schema(query=Query, extensions=[ParserCache()])
+
+    query = "query { hello"
+
+    result = schema.execute_sync(query)
+
+    assert len(result.errors) == 1
+    assert result.errors[0].message == "Syntax Error: Expected Name, found <EOF>."
+    assert result.errors[0].locations == [SourceLocation(line=1, column=14)]
+    assert mock_parse.call_count == 1
+
+
 @patch("strawberry.schema.execute.parse", wraps=parse)
 def test_parser_cache_extension_max_size(mock_parse):
     @strawberry.type
diff --git a/tests/schema/test_execution_errors.py b/tests/schema/test_execution_errors.py
--- a/tests/schema/test_execution_errors.py
+++ b/tests/schema/test_execution_errors.py
@@ -38,10 +38,13 @@ async def name(self) -> str:
         }
     """
 
-    with pytest.raises(RuntimeError) as e:
-        schema.execute_sync(query)
-
-    assert e.value.args[0] == "GraphQL execution failed to complete synchronously."
+    result = schema.execute_sync(query)
+    assert len(result.errors) == 1
+    assert isinstance(result.errors[0].original_error, RuntimeError)
+    assert (
+        result.errors[0].message
+        == "GraphQL execution failed to complete synchronously."
+    )
 
 
 @pytest.mark.asyncio
diff --git a/tests/types/test_execution.py b/tests/types/test_execution.py
--- a/tests/types/test_execution.py
+++ b/tests/types/test_execution.py
@@ -1,5 +1,3 @@
-import pytest
-
 import strawberry
 from strawberry.extensions import SchemaExtension
 
@@ -150,8 +148,10 @@ def on_operation(self):
 
     schema = strawberry.Schema(Query, extensions=[MyExtension])
 
-    with pytest.raises(RuntimeError):
-        schema.execute_sync("mutation { myMutation }")
+    result = schema.execute_sync("mutation { myMutation }")
+    assert len(result.errors) == 1
+    assert isinstance(result.errors[0].original_error, RuntimeError)
+    assert result.errors[0].message == "No GraphQL document available"
 
 
 def test_error_when_accessing_operation_type_with_invalid_operation_name():
@@ -165,5 +165,7 @@ def on_parse(self):
 
     schema = strawberry.Schema(Query, extensions=[MyExtension])
 
-    with pytest.raises(RuntimeError):
-        schema.execute_sync("query { ping }", operation_name="MyQuery")
+    result = schema.execute_sync("query { ping }", operation_name="MyQuery")
+    assert len(result.errors) == 1
+    assert isinstance(result.errors[0].original_error, RuntimeError)
+    assert result.errors[0].message == "Can't get GraphQL operation type"