Skip to content

IDbCommandInterceptor.CommandFailed does not capture all command exceptions #35229

Description

@fruitraccoon

Context

When using EF Core together with Sql Server, we catch exceptions from the database (mostly SqlExceptions) and convert them to domain-specific custom exceptions. Originally we were doing this as a try/catch inside an override of SaveChangesAsync in our DbContext.

This approach worked fine for most cases, however when we execute a stored procedure via a call like DbContext.Database.ExecuteSqlInterpolatedAsync, it does not use SaveChangesAsync, and so exceptions were not being converted in those cases.

In an attempt to create one encapsulated thing that handles the exception handling, I implemented an IDbCommandInterceptor using the CommandFailed and CommandFailedAsync methods.

This appeared to work as we wanted and handled regular calls (made via SaveChangesAsync) and stored procedure calls (and other ad-hoc calls) made via DbContext.Database.

However, we've come across an edge case where the IDbCommandInterceptor approach is not working.

  • When changes to multiple entities have been made and then SaveChangesAsync is called, EF can decide to make some optimisations. This includes executing a batch of statements in a single database call (which makes sense to do).
  • When it does this, it appears that if the first statement of the batch causes an exception, then everything works as expected - the call fails and the exception is passed to the CommandFailedAsync of the interceptor.
  • However, if the first statement succeeds and returns a result set (such as a new timestamp version column value), but then one of the subsequent statements throws an exception, then the CommandFailedAsync of the interceptor is not called.
    • The end result is still an exception coming from DbContext.SaveChangesAsync, it just isn't captured into CommandFailedAsync.

After debugging into the code, it appears that this is because:

  • the batch statement is being executing as a Db Reader (which makes sense)
  • the result DbDataReader is only fully processed (via calling DbDataReader.NextResultAsync) after the handling for the interceptor is already passed. (NextResultAsync is called in AffectedCountModificationCommandBatch.cs in the ConsumeAsync method)

(Note: I found this blog post helpful in getting to this understanding: https://www.dbdelta.com/the-curious-case-of-undetected-sql-exceptions/)

My Question

After all the context, my question is:

Is this expected behaviour?

After looking at what the code is doing, it makes sense that in its current form that it behaves this way, but when I was reading about interceptors I had the impression that all command exceptions would be captured into the CommandFailed methods.

If it is intended to work this way, perhaps some additional documentation in the Interceptors page explaining that not all exceptions will be captured would help?

But ideally of course, it would be great if interceptors could be updated/enhanced so that an exception that appears as part of processing the DbDataReader would also be caught.

Issue Reproduction code

As this involves both C# code and Sql Server code and to convince EF that it needs to use its batch command optimisation, this would be quite difficult to create a reproduction for. However, hopefully the explanation above makes sense and is straight-forward to understand.

Existing Issues Raised

None found on this topic.

Work-around

In order to get the behaviour we're after, we have changed our code to have handlers in both the interceptor and within our override of SaveChangesAsync. This was not a difficult change to make (if a little less neat) but we've only just found out, with a bunch of debugging, that we needed this change and why.

Alternative Work-around not used

An alternative that was considered was to process the DbDataReader within the ReaderExecutedAsync similar to what is described within this issue.

However this involves reading the entire results of every query into memory, which could defeat the purpose of using a reader for large results sets, and it has more risk of inadvertently affecting some other part of how EF works. Another minor point is that DataTable.Load does not support async calls, so more custom code would be required to implement that.

Version information

EF Core version: <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
Target framework: net8.0

Activity

  1. ajcvickers commented on Nov 29, 2024

    @ajcvickers
    Contributor

    Note for team: this is an interesting case. So far, we only do CommandFailed when the error happens on the call to actually execute the command. If this succeeds, but then there is a failure reading the result set, then we handle this differently. This usually makes sense for queries, but I agree with the poster that this is both confusion and a potential pit of failure for using SaveChanges with the CommandFailed interceptor.

  2. added this to the Backlog milestone on Nov 29, 2024
  3. added theissue type on Jun 15, 2025
  4. AdCodicem commented on Oct 16, 2026

    @AdCodicem

    The query side has the same shape, and can be shown without SQL Server: an error the database raises after the first row (so from DbDataReader.Read, not from ExecuteReader) reaches no IDbCommandInterceptor method. The interceptor sees ReaderExecuted, DataReaderClosing and DataReaderDisposing, as for a successful query; only the logger gets QueryIterationFailed.

    Repro (SQLite, in memory): https://github.com/AdCodicem/dotnet-upstream-repros/tree/main/efcore-interceptor-reader-error. SQLite raises integer overflow for abs(-9223372036854775808) on the row it reaches, so the same two rows ordered one way or the other move the error from ExecuteReader to Read:

    1. The failing row is the first one (raised by ExecuteReader):
       logged: Microsoft.EntityFrameworkCore.Database.Command.CommandError
       interceptor: CommandFailed (SqliteException: SQLite Error 1: 'integer overflow'.)
       logged: Microsoft.EntityFrameworkCore.Query.QueryIterationFailed
       caught by the caller: SqliteException: SQLite Error 1: 'integer overflow'.
    
    2. The failing row is the second one (raised by DbDataReader.Read):
       interceptor: ReaderExecuted
       logged: Microsoft.EntityFrameworkCore.Query.QueryIterationFailed
       interceptor: DataReaderClosing
       interceptor: DataReaderDisposing
       caught by the caller: SqliteException: SQLite Error 1: 'integer overflow'.
    

    Same output on 10.0.13 and 11.0.0-rc.1.26425.128. On the query side, RelationalCommand.ExecuteReader reports CommandError from its catch around DbCommand.ExecuteReader (

    catch (Exception exception)
    {
    if (Dependencies.ExceptionDetector.IsCancellation(exception))
    {
    logger?.CommandCanceled(
    connection,
    command,
    LogCommandText,
    context,
    DbCommandMethod.ExecuteReader,
    commandId,
    connection.ConnectionId,
    startTime,
    stopwatch.Elapsed,
    parameterObject.CommandSource);
    }
    else
    {
    logger?.CommandError(
    connection,
    command,
    LogCommandText,
    context,
    DbCommandMethod.ExecuteReader,
    ), while an error from Read is caught by the enumerator and only logged as QueryIterationFailed (
    catch (Exception exception)
    {
    if (_exceptionDetector.IsCancellation(exception))
    {
    _queryLogger.QueryCanceled(_contextType);
    }
    else
    {
    _queryLogger.QueryIterationFailed(_contextType, exception);
    ).

    So whether CommandFailed fires for the same server error depends on where the provider surfaces it, which an interceptor written to translate or count database errors cannot tell from the outside. If raising CommandFailed there is not wanted, could the CommandFailed docs (and the "Command failures" row of https://learn.microsoft.com/ef/core/logging-events-diagnostics/interceptors) say that it covers only errors raised while executing the command, not those raised while EF Core reads the results, and point to ISaveChangesInterceptor.SaveChangesFailed and CoreEventId.QueryIterationFailed for the rest?

    Investigation and write-up assisted by Claude (Anthropic); the linked repro was run on Windows 11 Pro (10.0.26200) with .NET SDK 10.0.402 and Microsoft.EntityFrameworkCore.Sqlite 10.0.13, and with .NET SDK 11.0.100-rc.1.26425.128 and Microsoft.EntityFrameworkCore.Sqlite 11.0.0-rc.1.26425.128, and I reviewed this comment.

  5. added a commit that references this issue on Oct 25, 2026
    4655b74
  6. Shivansh-Gaur2 commented on Oct 25, 2026

    @Shivansh-Gaur2
    Contributor

    @ajcvickers, PR #39405 proposes reporting provider Read/ReadAsync and NextResult/NextResultAsync failures through the existing CommandFailed callbacks. Cancellations use CommandCanceled; EF row materialization exceptions remain on the query-iteration path, and QueryIterationFailed is preserved.

    The PR covers streaming and buffered consumption, the SaveChanges result-advancement case, cancellation, and the materialization boundary, including the SQLite abs(long.MinValue) reproduction. Logging, event-specific diagnostic listeners, and interceptors are tested independently. The focused relational suites pass 157 tests, and the SQLite command-interception suite passes 300 tests.

    Could you confirm whether this is the direction the team wants for the issue?

  7. added a commit that references this issue on Oct 30, 2026
    6990229
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions