hannasm
hannasm

Reputation: 2041

Attach StackTrace To Exception Without Throwing in C# / .NET

I have a component which handles errors using return values as opposed to standard exception handling. In addition to the error code, it also returns a stack trace of where the error has occurred. A wrapper I use to invoke the component will interpret return codes and throw an exception.

I'd like to have the wrapper throw an exception that includes the captured stack trace information from the component. I'd like it to appear as if the exception had been thrown from the original site of the error even though it was thrown elsewhere. More specifically, I'd like the stack trace displayed by the Visual Studio test runner to reflect the proper location.

Is there any way to do this? It would also be nice if I could avoid low-level reflection tricks accessing private members but I will take what I can get.

I'm not concerned with how to capture a stack trace, I'm concerned with attaching a stack trace that's already been captured, to an Exception.

I tried overriding the StackTrace property, but Visual Studio is pulling the stack trace data from somewhere else, and seems to ignore the overridden property completely

CustomException GenerateExcpetion()
{
    return new CustomException();
}

void ThrowException(Exception ex)
{
    Trace.WriteLine("Displaying Exception");
    Trace.WriteLine(ex.ToString());
    var edi = ExceptionDispatchInfo.Capture(ex);
    edi.Throw();
}

[TestMethod]
public void Test006()
{
    var ex = GenerateExcpetion();
    ThrowException(ex);
}

public class CustomException : Exception
{
    string _stackTrace;
    public CustomException()
    {
        _stackTrace = Environment.StackTrace;
    }

    public override string StackTrace
    {
        get
        {
            return base.StackTrace;
        }
    }
}

The Exception.ToString() method pulls stack trace data from a private property, and so the stack trace coming from the override doesn't get shown.

CustomException: Exception of type 'CustomException' was thrown.

The ExceptionDispatchInfo looks for the stack trace data from a private property as well, and so it can't find any of that data, and when you throw this custom exception a new stack trace is attached to the exception with the location that it is being thrown from. If you use throw directly, the private stack information is set to the place where the throw happened.

Upvotes: 57

Views: 34665

Answers (8)

0xced
0xced

Reputation: 26518

Here's a modern version of hannasm's answer that leverages the new [UnsafeAccessor] attribute, thus requiring .NET 8.

using System.Runtime.CompilerServices;

namespace System;

internal static class ExceptionExtensions
{
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_stackTraceString")]
    private static extern ref string _stackTraceString(Exception exception);

    public static Exception SetStackTrace(this Exception exception, string stackTrace)
    {
        _stackTraceString(exception) = stackTrace;
        return exception;
    }
}

See Accessing private members without reflection in C# by Gérald Barré (a.k.a. Meziantou) for more information.

If you don't mind having an extra line with --- End of stack trace from previous location --- at the end of the provided stack trace string then using ExceptionDispatchInfo.SetRemoteStackTrace(exception, stackTrace) as suggested by Zdeněk Jelínek is probably a better solution since it uses only a public API and doesn't access a private field.

Upvotes: 0

Zdeněk Jelínek
Zdeněk Jelínek

Reputation: 2913

Since .NET 5, there is ExceptionDispatchInfo.SetCurrentStackTrace which populates an exception with the current stack trace.

.NET 6 additionally introduced ExceptionDispatchInfo.SetRemoteStackTrace which takes an arbitrary string.

Note, however, that these methods set the _remoteStackTraceString field on the Exception type which is opposed to the other answers here, where the _stackTraceString field is typically set. The resulting behavior may therefore be different but should suffice for the purpose of the original question.

Upvotes: 6

Marcel
Marcel

Reputation: 1845

There is an even more sophisticated solution that avoids any runtime overhead of the reflection part:

public static class ExceptionExtensions
{
    public static Exception SetStackTrace(this Exception target, StackTrace stack) => _SetStackTrace(target, stack);

    private static readonly Func<Exception, StackTrace, Exception> _SetStackTrace = new Func<Func<Exception, StackTrace, Exception>>(() =>
    {
        ParameterExpression target = Expression.Parameter(typeof(Exception));
        ParameterExpression stack = Expression.Parameter(typeof(StackTrace));
        Type traceFormatType = typeof(StackTrace).GetNestedType("TraceFormat", BindingFlags.NonPublic);
        MethodInfo toString = typeof(StackTrace).GetMethod("ToString", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { traceFormatType }, null);
        object normalTraceFormat = Enum.GetValues(traceFormatType).GetValue(0);
        MethodCallExpression stackTraceString = Expression.Call(stack, toString, Expression.Constant(normalTraceFormat, traceFormatType));
        FieldInfo stackTraceStringField = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance);
        BinaryExpression assign = Expression.Assign(Expression.Field(target, stackTraceStringField), stackTraceString);
        return Expression.Lambda<Func<Exception, StackTrace, Exception>>(Expression.Block(assign, target), target, stack).Compile();
    })();
}

Upvotes: 8

user4388177
user4388177

Reputation: 2613

You can use: Environment.StackTrace to capture the stacktrace at the occurence of the error in the component and then return it together with the other error informations or rethrow.

You can manually build stackframes to create a full trace. See StackFrame/StackTrace for more information.

Upvotes: 15

PARUS
PARUS

Reputation: 113

var error = new Exception(ex.Message);

var STACK_TRACE_STRING_FI = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance);

STACK_TRACE_STRING_FI.SetValue(error, ex.StackTrace);

Upvotes: -3

Mayank
Mayank

Reputation: 8852

For me following seems to be the best way to do it. This approach works well with loggers since .ToString() for Exception uses private field for stack trace.

public class CustomException : Exception
{
    public CustomException()
    {
        var stackTraceField = typeof(CustomException).BaseType
            .GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);

        stackTraceField.SetValue(this, Environment.StackTrace);
    }
}

Upvotes: 7

hannasm
hannasm

Reputation: 2041

Well with nothing elegant available, here is my reflection based approach.

public static class ExceptionUtilities
{
    private static readonly FieldInfo STACK_TRACE_STRING_FI = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance);
    private static readonly Type TRACE_FORMAT_TI = Type.GetType("System.Diagnostics.StackTrace").GetNestedType("TraceFormat", BindingFlags.NonPublic);
    private static readonly MethodInfo TRACE_TO_STRING_MI = typeof(StackTrace).GetMethod("ToString", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { TRACE_FORMAT_TI }, null);

    public static Exception SetStackTrace(this Exception target, StackTrace stack)
    {
        var getStackTraceString = TRACE_TO_STRING_MI.Invoke(stack, new object[] { Enum.GetValues(TRACE_FORMAT_TI).GetValue(0) });
        STACK_TRACE_STRING_FI.SetValue(target, getStackTraceString);
        return target;
    }
}

Writing a formatted StackTrace string to the _stackTraceString property seems to be enough to fool visual studio test runner and the Exception.ToString() methods into believing the stack was generated by a throw (without actually throwing anything).

See below for usage:

    StackTrace GetDeeperStackTrace(int depth)
    {
        if (depth > 0)
        {
            return GetDeeperStackTrace(depth - 1);
        }
        else
        {
            return new StackTrace(0, true);
        }
    }

    [TestMethod]
    public void Test007()
    {
        Exception needStackTrace = new Exception("Some exception");
        var st = GetDeeperStackTrace(3);

        needStackTrace.SetStackTrace(st);

        Trace.Write(needStackTrace.ToString());

        throw new Exception("Nested has custom stack trace", needStackTrace);
    }

Upvotes: 12

adjan
adjan

Reputation: 13652

Just create your own Exception type and override the StackTrace property:

class MyException : Exception
{
    private string oldStackTrace;

    public MyException(string message, string stackTrace) : base(message)
    {
        this.oldStackTrace = stackTrace;
    }


    public override string StackTrace
    {
        get
        {
            return this.oldStackTrace;
        }
    }
}

Upvotes: 49

Related Questions