Skip to content

Result Class API and Some Implementation Details #87

@DScheglov

Description

@DScheglov

Instantiation

Minimize Direct Instantiation API Surface

The proposal currently offers four ways to instantiate results: two direct and two indirect.

  1. Static factory methods (direct):

    const okResult = Result.ok(value);
    const errResult = Result.error(error);
  2. Constructor (direct):

    const okResult = new Result(true, undefined, value);
    const errResult = new Result(false, error);
  3. Static try method on a non-function/non-promise (indirect):

    const okResult = Result.try(value);
  4. try operator on a non-function/non-promise (indirect):

    const okResult = try value;

Indirect ways can be flagged by linters as suboptimal. The preferred approach is the static factory methods (Result.ok, Result.error), which are concise and less error-prone.

The constructor approach introduces problems, especially for JS (non-TS) developers:

  • value and error can be swapped, creating incorrect results.

  • Using two arguments for errors encourages incorrect use for success cases.

  • Ambiguity in specifying ok, error, and value:

    • ok: true/false, 1/0, "success"/"" etc.
    • error in ok-results: null, undefined, any ignored value.
    • value in error-results: same ambiguity.
  • Non-intuitive behavior when both error and value are set.

Explicit new Result(...) must throw an error:

const result = new Result(...); // throws
const result = new (Result.ok()).constructor(...); // throws

Void Results

Factory methods must allow creating results without values:

const voidOk = Result.ok();
const voidErr = Result.error();

Useful when details don’t matter.

Instance Check

Both checks must return true:

Result.ok(..) instanceof Result
Result.error(..) instanceof Result

Properties

Result instances have three properties:

  1. ok
  2. error (only if ok is false)
  3. value (only if ok is true)

Properties must be readonly. This prevents inconsistent states, e.g., ok true but with an error, or vice versa. Instances should also be sealed to prevent adding error to ok-results or value to error-results. This is especially important if discriminating with 'error' in result or 'value' in result is supported.

Instance Methods

Algebraic Data Types

Result models success/failure flows similar to other languages, supporting:

  • Functor
  • Monad
  • Bi-Functor
  • Bi-Monad

Required methods (like in Array):

class Result {
  map(mapValueFn, mapErrorFn?) {}
  mapError(mapErrorFn) {}

  flatMap(chainValueFn, chainErrorFn?) {}
  flatMapError(chainErrorFn) {}

  flat() {}
}

Optional combined naming (like in Promise):

class Result {
  andThen(handleValue, handleError?) {}
  orElse(handleError) {}
}

Unwrapping Methods

Direct field access is preferred, but unwrapping is useful, especially for TS developers:

const value = result.unwrap(); // returns value or throws TypeError with cause=result

Unwrap methods:

class Result {
  unwrap(options?) {}
  unwrapError(options?) {}
}

options can:

  • Provide a default:

    result.unwrap({ default: 'fallback' });
  • Provide an expected error message:

    result.unwrap({ expect: 'Error Message' });
  • Handle error explicitly:

    result.unwrap({ onError(error) {
      console.error(error);
      return 'fallback';
    }});

A match method is also recommended:

class Result {
  match(onOk, onError) {}
}

Example:

app.post('/some-resource', (req, res) => {
  res.json(
    someService.doSomething().match(
      value => ({ status: 'success', data: value }),
      error => ({ status: 'error', error: error.message }),
    ),
  );
});

Static Methods

Currently proposed static methods:

  • ok
  • error
  • try

Additional methods for arrays/tuples of results:

  • collect – returns an ok result with an array of values if all are ok, or the first non-ok result.
  • collectRight – same as collect, but returns the last non-ok result instead of the first.

Implementation

It is recommended to implement two separate classes instead of one generic Result:

  • OkResult – represents a successful result.
  • ErrorResult – represents an error result.

This simplifies method implementations by removing the need for branching logic inside every method. Each subclass can implement its own version of map, flatMap, unwrap, etc., without runtime checks. Factory methods (Result.ok, Result.error) return instances of these classes, but they share a common base class Result to ensure instanceof Result works consistently.

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions