Skip to content

mapOk sync throw creates an unhandled rejection and leaves the Future permanently unsettled #101

@baptou12

Description

@baptou12

When the callback passed to mapOk throws synchronously, the error escapes as an unhandled rejection. The resulting Future never resolves, making it impossible for any downstream consumer to handle the error.

Reproduction

import { Future, Result } from "@swan-io/boxed";

process.on("unhandledRejection", (e) =>
  console.log("unhandledRejection:", e),
);

const future = Future.fromPromise(Promise.resolve({ name: null }))
  .mapOk((data) => data.name.toUpperCase()); // throws TypeError

// None of these ever fire:
future.tap((result) => console.log("tap:", result));
future.toPromise().then(console.log).catch(console.log);
future.resultToPromise().then(console.log).catch(console.log);

// Output: unhandledRejection: TypeError: Cannot read properties of null (reading 'toUpperCase')
// → tap, toPromise, resultToPromise never settle

What happens

When the callback passed to mapOk throws, the exception escapes the Future abstraction. The error becomes an unhandled Promise rejection and the derived Future never settles, leaving downstream consumers hanging.

Expected behavior

The throw should be caught and the Future should resolve with Result.Error(thrownError), so that .mapError, .tap, .resultToPromise etc. can handle it normally.

Workaround

Using mapOkToResult + Result.fromExecution works:

Future.fromPromise(Promise.resolve({ name: null }))
  .mapOkToResult((data) =>
    Result.fromExecution(() => data.name.toUpperCase()),
  )
  .tap(console.log);
// → Result.Error(TypeError: Cannot read properties of null ...)

Suggested fix

Make mapOk internally delegate to mapOkToResult + Result.fromExecution:

mapOk(func) {
  return this.mapOkToResult((value) =>
    Result.fromExecution(() => func(value))
  );
}

Same issue likely applies to map, flatMapOk, and other methods that invoke user callbacks during resolution.

Note: since JavaScript allows throwing arbitrary values, this would result in Result<_, unknown>, consistent with Result.fromExecution.

Context

We hit this in a NestJS/Fastify backend. A corrupted database record caused mapOk to throw during entity mapping. The HTTP request hung indefinitely because the Future never settled and no response was sent. The only trace was a logged unhandled rejection.

Version: 3.2.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions