Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StackTrace when catch the exception #1201

Open
NgaiKaKit opened this issue Feb 27, 2023 · 6 comments
Open

StackTrace when catch the exception #1201

NgaiKaKit opened this issue Feb 27, 2023 · 6 comments

Comments

@NgaiKaKit
Copy link

We are using this library for over four years, and we love it. We almost use language.ext around 99% in our coding.
And now we are facing the issue that hard to tracing the code calling stack when there is exception throw.

For example in following code

using static LanguageExt.Prelude;
using LanguageExt;
using LanguageExt.Sys.Live;


static Aff<Runtime, int> F1()  => F2();
static Aff<Runtime, int> F2() => F3();
static Aff<Runtime, int> F3() => F4();

static Aff<Runtime, int> F4() =>
    from r in SuccessAff(1)
    from r2 in guard(false, () => throw new Exception())
    select r;
try
{
    F1().Run(Runtime.New()).Result.ThrowIfFail();
}
catch (Exception e)
{
    Console.WriteLine(e);
    throw;
}

And here is the exception's stackTrace.

   at Program.<>c.<<Main>$>b__0_6() in C:\Users\NgaiKit\RiderProjects\ConsoleApp1\ConsoleApp1\Program.cs:line 14
   at LanguageExt.AffGuards.ToAff(Guard`1 ma)
   at LanguageExt.AffGuards.<>c__DisplayClass8_0`2.<SelectMany>b__0(A a)
   at LanguageExt.AffExtensions.<>c__DisplayClass148_0`2.<<Bind>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at LanguageExt.Aff`1.Run()
   at LanguageExt.ExceptionExtensions.Rethrow(Exception exception)
   at LanguageExt.Common.Error.Throw()
   at LanguageExt.Fin`1.ThrowIfFail()
   at Program.<Main>$(String[] args) in C:\Users\NgaiKit\RiderProjects\ConsoleApp1\ConsoleApp1\Program.cs:line 18

When we get the exception in our system log, we can only know It is throw in the line 14 (F4()), and the entry location is from line 18,
But we cannot getting the function calling stack for it actually calling the path F1 => F2 => F3 => F4.

Is there any suggestion on how to improve the code so that we can get the actual function calling stack when we got the exception

@NgaiKaKit NgaiKaKit changed the title StackTrace on exception StackTrace when catch the exception Feb 27, 2023
@timmi-on-rails
Copy link
Contributor

timmi-on-rails commented Mar 1, 2023

The method call to F1() immediately yields the return value of F4().
Calling F1 does not throw the exception.
Now, when you Run the returned Aff there is no way to report where the Aff came from (F1 => F2 => F3 => F4).

Now let's assume that you just accidentally reduced your example too much.
Here is an example that actually has a deeper call stack, where F2, F3, F4 are evaluated lazily:

using static LanguageExt.Prelude;
using LanguageExt;
using LanguageExt.Sys.Live;


static Aff<Runtime, int> F1() => unitAff.Bind(_ => F2());
static Aff<Runtime, int> F2() => unitAff.Bind(_ => F3());
static Aff<Runtime, int> F3() => unitAff.Bind(_ => F4());

static Aff<Runtime, int> F4() =>
    from r in SuccessAff(1)
    from r2 in guard(false, () => throw new Exception())
    select r;
try
{
    F1().Run(Runtime.New()).Result.ThrowIfFail();
}
catch (Exception e)
{
    Console.WriteLine(e);
    throw;
}

I am afraid this example won't show a better stack trace.
It's because the thrown exception in your guard will be catched and wrapped into the result (Fin<int>) of the Aff, when it is run.
The extension method ThrowIfFail() will simply rethrow the wrapped exception.

I am not aware of a general way to get more details where the failed effect came from.
In the past I also came across this disadvantage of wrapping exceptions.
I would be interested, if there is a solution?

@MrWuffels
Copy link

Well, this is the actual callstack, therefor there's no way to achieve a better one using monads. However, error handling with monads works differently. For example, instead of just throwing a random exception in your guard, try to create a meaningful error which tells you what was wrong, or if you can't, where it went wrong

@bmazzarol
Copy link
Contributor

Collecting information (sort of analogous to unwinding the stack) as the Error moves through the bind chains is not impossible, just not necessarily going to provide useful information.

Using compiler attributes you can get the line number and source file of the invokation point of the Error, it might be a starting point for something better.

I know that ZIO has built in support for tracing errors produced under concurrent loads. Would have to investigate how that works and if can be replicated.

@timmi-on-rails
Copy link
Contributor

Here is an approach, which I don't recommend:

using static LanguageExt.Prelude;
using LanguageExt;
using LanguageExt.Sys.Live;
using System.Runtime.CompilerServices;
using LanguageExt.Common;

static Aff<Runtime, int> F1() => unitAff.Bind(_ => F2()).MapFail(e => AddTraceInfo(e));
static Aff<Runtime, int> F2() => unitAff.Bind(_ => F3()).MapFail(e => AddTraceInfo(e));
static Aff<Runtime, int> F3() => unitAff.Bind(_ => F4()).MapFail(e => AddTraceInfo(e));

static Aff<Runtime, int> F4()
    => (from r in SuccessAff(1)
        from r2 in guard(false, () => throw new Exception())
        select r).MapFail(e => AddTraceInfo(e));

static Error AddTraceInfo(
    Error error,
    [CallerLineNumber] int lineNumber = 0,
    [CallerMemberName] string? caller = null)
    => error.Append($"Failed in {caller} line {lineNumber}.");

try
{
    F1().Run(Runtime.New()).Result.ThrowIfFail();
}
catch (Exception e)
{
    Console.WriteLine(e);
    throw;
}

Output:

System.AggregateException: One or more errors occurred. (Exception of type 'System.Exception' was thrown.) (Failed in <Main>$ line 14.) (Failed in <Main>$ line 9.) (Failed in <Main>$ line 8.) (Failed in <Main>$ line 7.)
 ---> System.Exception: Exception of type 'System.Exception' was thrown.
   at Program.<>c.<<Main>$>b__0_14() in C:\Users\Tom\Documents\Projects\langexttrace\langexttrace\Program.cs:line 13
   at LanguageExt.AffGuards.ToAff(Guard`1 ma)
   at LanguageExt.AffGuards.<>c__DisplayClass8_0`2.<SelectMany>b__0(A a)
   at LanguageExt.AffExtensions.<>c__DisplayClass148_0`2.<<Bind>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at LanguageExt.Aff`1.Run()
   --- End of inner exception stack trace ---
   at LanguageExt.ExceptionExtensions.Rethrow(Exception exception)
   at LanguageExt.Fin`1.ThrowIfFail()
   at Program.<Main>$(String[] args) in C:\Users\Tom\Documents\Projects\langexttrace\langexttrace\Program.cs:line 24
 ---> (Inner Exception #1) Failed in <Main>$ line 14.<---

 ---> (Inner Exception #2) Failed in <Main>$ line 9.<---

 ---> (Inner Exception #3) Failed in <Main>$ line 8.<---

 ---> (Inner Exception #4) Failed in <Main>$ line 7.<---

@bmazzarol
Copy link
Contributor

What would be the best outcome for the developer debugging the code?

Some things that I would want,

  1. Original source of the failure. Not the call site where the effect was run
  2. Clear and consise contextual information
  3. A failure reason that combines with the other information and source to pinpoint and resolve the issue

All three can be accomplished with no fundamental library changes,

  1. When constructing the error, use source attributes to collect the code line and file details. The attribute can be a bit painful, but with some care should collect the correct source details
  2. With the latest changes to Error which enables subtyping it, more information can be attached, use this to provide context
  3. Lastly spend the time to construct a failure reason the ties it all together

I don't think I would miss stack traces if I had access to all that.

The issue might still be that the developer who has no understanding of effects, would still be thrown by the stack trace, end up on the wrong line and waste time. Not sure I have the solution to that, but might be another area to explore.

@timmi-on-rails
Copy link
Contributor

But isn't the problem, that there is not enough contextual information without a stack trace replacement?

I'd imagine having a File.move effect, which might be used in many places.
If it fails with source file xyz does not exist and the source-code file and line information, where the error is created are known, then you are still lost in what location the effect was used/constructed.
Ofcourse you could try to guess based on the file name.
But that is it or am I missing something?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants