question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

How to best emulate Rust's `?` operator?

See original GitHub issue

I assume you are familiar with ? operator in Rust? I’m wondering what is the best way to achieve something similar with neverthrow’s ResultAsync.

REST APIs often require to execute a chain of requests that depend on each other, i.e., I cannot spawn them independently but need to await a successful result sequentially. In case of an error, I want to translate the error into a different error type. Here’s an example:

type ResponseType = {
  some: {
    nested: {
      field: number
    }
  },
  other: {
    nested: {
      field: number
    }
  }
}

async function exampleRequest(arg?: number): Promise<ResponseType> {
  return Promise.resolve({
    some: {nested: {field: 1}},
    other: {nested: {field: 2}},
  });
}

function wrap<T>(promise: Promise<T>): ResultAsync<T, Error> {
  return ResultAsync.fromPromise(promise, (e) => e as Error);
}

async function requestChain(): Promise<Result<number, string>> {

  let aResult = await wrap(exampleRequest())
  if (aResult.isErr()) {
    return err("something went wrong in request A")
  }
  let a = aResult.value.some.nested.field

  let bResult = await wrap(exampleRequest(a))
  if (bResult.isErr()) {
    return err("something went wrong in request B")
  }
  let b = bResult.value.other.nested.field

  let cResult = await wrap(exampleRequest(b))
  if (cResult.isErr()) {
    return err("something went wrong in request C")
  }
  let c = cResult.value.other.nested.field

  return ok(a + b + c)
}

It looks like I’m looking for some kind of unwrapOrReturnError. In Rust such chains simplify a lot when using the ? operator. Any thoughts what would be a nice solution with neverthrow?

I feel simply using map doesn’t scale so well, because each request would add a level of indentation (in the specific API case I had a chain of 8 requests). Using andThen seems tricky as well, because the steps have complex interdependencies, e.g. I would need the results of step 3+4 in step 7, but step 1+3 in step 8 or so.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:6 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
bluenote10commented, Nov 19, 2020

Here is a full demo of the hoisting approach:

async function exampleRequest(arg?: number): Promise<ResponseType> {
  return Promise.resolve(42);
}

type WrappedError = {
  msg: string,
  originalError: Error,
}

function wrapPromise<T>(promise: Promise<T>, msg: string): ResultAsync<T, WrappedError> {
  return ResultAsync.fromPromise(promise, (e) => ({
    msg: msg,
    originalError: e as Error
  }));
}

function startChain(): ResultAsync<null, WrappedError> {
  return okAsync(null)
}

function requestChain(): ResultAsync<number, WrappedError> {

  // predefine stuff that should be remembered across requests
  let a: number
  let b: number
  let c: number

  return startChain().andThen(() => {
    return wrapPromise(
      exampleRequest(),
      "Something failed in request A",
    )
  }).andThen(result => {
    a = result
    return wrapPromise(
      exampleRequest(a),
      "Something failed in request B",
    )
  }).andThen(result => {
    b = result
    return wrapPromise(
      exampleRequest(a + b),
      "Something failed in request C",
    )
  }).andThen(result => {
    c = result
    return ok(a + b + c)
  })
}

A few notes:

  • This feels pretty good in practice despite the ugly hoisting. Practically it gets close to Rust’s ? operator in the sense that the function short-circuits on error, all values are naturally unwrapped, and it easily scales to a large number to-be-unwrapped values.
  • Nice that I no longer need any async.
  • The startChain is obviously not necessary, I just liked to make the first request syntactically identical to the others.
  • Similarly the last andThen could also be a plain map returning just the value.
  • I’ve also extended the example to show what my actual intent is in terms of error handling: In practice I find it useful to have a WrappedError type that allows me to introduce my own error message (which could be exposed to the user) along with storing the original errors (which I would use for debug logging or so).

EDIT: One of your examples inspired me to re-write that using destructuring, which also looks pretty good:

const requestChain: () => ResultAsync<number, WrappedError> = () => (
  startChain().andThen(() =>
    wrapPromise(
      exampleRequest(),
      "Something failed in request A",
    ).map(result => ({
      a: result, // extract `a` here instead
    }))
  ).andThen(({a}) =>
    wrapPromise(
      exampleRequest(a),
      "Something failed in request B",
    ).map(result => ({
      a: a,      // forward
      b: result, // extract `b` here instead
    }))
  ).andThen(({a, b}) =>
    wrapPromise(
      exampleRequest(a + b),
      "Something failed in request C",
    ).map(result => ({
      a: a,      // forward
      b: b,      // forward
      c: result, // extract `c` here instead
    }))
  ).andThen(({a, b, c}) =>
    ok(a + b + c)
  )
)

Is the above what you had in mind?

Yes, exactly. I assume the nested tuple approach would work with a similar destructuring on input side, but would perform the “accumulation” automatically, i.e., it spares the explicit map’s in the last example.

(From my perspective the current API allows for a good enough approximation of the ? operator, so feel free to close the issue.)

0reactions
supermacrocommented, Nov 20, 2020

I added a new issue (https://github.com/supermacro/neverthrow/issues/186) to track the *Collect api’s. Not yet sure if it can even be done in a way that allows for flat tuples.

Read more comments on GitHub >

github_iconTop Results From Across the Web

What's your favorite weird rust operator? - Reddit
Conceptually a better way of thinking about it is that the value is moved from right to left, and the right becomes a...
Read more >
Emulating the Rust borrow checker with C++ move-only types
The Rust borrow checker checks borrows: that is, references. This doesn't emulate borrows, but rather emulates Rust-style moves.
Read more >
Operator Overloading - Rust By Example
In Rust, many of the operators can be overloaded via traits. That is, some operators can be used to accomplish different tasks based...
Read more >
How to simulate Rust's Mutex<Object> in C++? - Stack Overflow
One way to do it is have your Mutex<T> only allow access to the contained T via a lambda: template <typename T> class...
Read more >
Object-Oriented Programming - A Gentle Introduction to Rust
Everyone comes from somewhere, and the chances are good that your previous programming language implemented Object-Oriented Programming (OOP) in a particular ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found