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

Code that outputs neverthrow ResultAsync promise-like objects cannot be traced or wrapped #4189

Open
timotheeg opened this issue Mar 26, 2024 · 0 comments

Comments

@timotheeg
Copy link

timotheeg commented Mar 26, 2024

neverthrow provides a Promise-like entity, with added APIs (e.g. .andThen(), .orElse() which break the way dd-trace identifies promises, and make some code not traceable/wrappable.

From neverthrow's readme:

Encode failure into your program.

This package contains a Result type that represents either success (Ok) or failure (Err).

For asynchronous tasks, neverthrow offers a ResultAsync class which wraps a Promise<Result<T, E>> and gives you the same level of expressivity and control as a regular Result<T, E>.

ResultAsync is thenable meaning it behaves exactly like a native Promise ... except you have access to the same methods that Result provides without having to await or .then the promise! Check out the wiki for examples and best practices.

Sample code to repro the issue (save to test.mjs):

import { err, ok, ResultAsync } from 'neverthrow'
import tracer from 'dd-trace'

tracer.init()

function bar() {
  return tracer.trace('bar', () => {
    return ResultAsync.fromPromise(
      Promise.resolve(1)
    )
  });
}

function foo() {
  bar().andThen(() => {
    console.log('foo.andthen');
  })
}

foo(); // throws with TypeError: bar(...).andThen is not a function

That's because dd-trace uses this code in tracer._tracer.trace does this here:

      if (result && typeof result.then === 'function') {
        return result.then(
          value => {
            span.finish()
            return value
          },
          err => {
            addError(span, err)
            span.finish()
            throw err
          }
        )
      } else {
        span.finish()
      }

So if result is a neverthrow Result object, it is identified as a promise (since it does contains a .then() method), and wrapped, breaking code that relies on it to remain a Result instance.

A "simple" fix could be to account for neverthrow Result specifically, perhaps like this:

    if (result && typeof result.andThen === 'function') {
      // Intercepting ResultAsync chains from NeverThrow
      return result
        .map(value => {
            span.finish()
            return value
        })
        .mapErr(error => {
          addError(span, error)
          span.finish()
          return error
        });
    } else if (result && typeof result.then === 'function') {
      // Intercepting "standard" Promise chains
      return result.then(
        value => {
          span.finish()
          return value
        },
        err => {
          addError(span, err)
          span.finish()
          throw err
        }
      )
    } else {
      span.finish()
    }
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

1 participant