promise

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package promise brings JavaScript's Promise vocabulary to Go: New mirrors `new Promise`, Await mirrors `await`, and All, Race, and AllSettled mirror the Promise combinators — while goroutines and context.Context run underneath.

// JS: const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);
user, orders, err := promise.All2(getUser(id), getOrders(id))

Callers never pass a context.Context. Each Promise owns an github.com/burrows99/async/abort.Controller; cancellation is reached through Promise.Abort. Because Go cannot preempt a goroutine, cancellation is opt-in exactly as in JavaScript: plain New work cannot be interrupted mid-flight, while work that wants to stop uses WithSignal and observes an abort.Signal — the Go analogue of threading { signal } into fetch.

Await returns (T, error); there is no Catch. A panic inside a task is recovered and surfaced as a *PanicError, so a task the package owns can never tear down the process. Errors compose with errors.Is and errors.As.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrNoPromises = fmt.Errorf("promise: no promises provided")

ErrNoPromises is returned by combinators such as Race that cannot produce a meaningful result from an empty set of promises. JavaScript's Promise.race over an empty array never settles; blocking forever is not useful in Go, so the combinators return this error instead.

View Source
var ErrTimeout = fmt.Errorf("promise: operation timed out: %w", context.DeadlineExceeded)

ErrTimeout is returned from Await when a Promise wrapped by Timeout does not settle before its deadline. It wraps context.DeadlineExceeded, so both errors.Is(err, ErrTimeout) and errors.Is(err, context.DeadlineExceeded) report true.

Functions

func All

func All[T any](ps ...*Promise[T]) ([]T, error)

All waits for every promise to settle and returns their values in input order. It is the analogue of JavaScript's Promise.all.

On the first failure, All aborts the remaining sibling tasks and returns a nil slice with that error — Promise.all's fail-fast behaviour, plus the sibling cancellation raw JavaScript lacks (siblings started with WithSignal stop promptly; plain New siblings finish and have their results discarded). With no promises, All returns a non-nil empty slice and a nil error.

Example

The JavaScript this mirrors:

const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);
package main

import (
	"fmt"

	"github.com/burrows99/async/promise"
)

func main() {
	sums, err := promise.All(
		promise.New(func() (int, error) { return 1, nil }),
		promise.New(func() (int, error) { return 2, nil }),
		promise.New(func() (int, error) { return 3, nil }),
	)
	fmt.Println(sums, err)
}
Output:
[1 2 3] <nil>

func All2

func All2[T1, T2 any](p1 *Promise[T1], p2 *Promise[T2]) (T1, T2, error)

All2 waits for 2 promises of independent types to settle and returns their values as a flat tuple. It is the heterogeneous, fixed-arity analogue of All: join a known number of differently-typed promises without boxing them into interface{}.

On the first failure, All2 aborts the sibling tasks and returns zero values for every result together with that error, matching All's fail-fast semantics.

Example

All2 joins promises of different types — the heterogeneous analogue of Promise.all that Go's generics cannot express as a single variadic call.

package main

import (
	"fmt"

	"github.com/burrows99/async/promise"
)

func main() {
	name, age, err := promise.All2(
		promise.New(func() (string, error) { return "ada", nil }),
		promise.New(func() (int, error) { return 36, nil }),
	)
	fmt.Println(name, age, err)
}
Output:
ada 36 <nil>

func All3

func All3[T1, T2, T3 any](p1 *Promise[T1], p2 *Promise[T2], p3 *Promise[T3]) (T1, T2, T3, error)

All3 waits for 3 promises of independent types to settle and returns their values as a flat tuple. It is the heterogeneous, fixed-arity analogue of All: join a known number of differently-typed promises without boxing them into interface{}.

On the first failure, All3 aborts the sibling tasks and returns zero values for every result together with that error, matching All's fail-fast semantics.

func All4

func All4[T1, T2, T3, T4 any](p1 *Promise[T1], p2 *Promise[T2], p3 *Promise[T3], p4 *Promise[T4]) (T1, T2, T3, T4, error)

All4 waits for 4 promises of independent types to settle and returns their values as a flat tuple. It is the heterogeneous, fixed-arity analogue of All: join a known number of differently-typed promises without boxing them into interface{}.

On the first failure, All4 aborts the sibling tasks and returns zero values for every result together with that error, matching All's fail-fast semantics.

func All5

func All5[T1, T2, T3, T4, T5 any](p1 *Promise[T1], p2 *Promise[T2], p3 *Promise[T3], p4 *Promise[T4], p5 *Promise[T5]) (T1, T2, T3, T4, T5, error)

All5 waits for 5 promises of independent types to settle and returns their values as a flat tuple. It is the heterogeneous, fixed-arity analogue of All: join a known number of differently-typed promises without boxing them into interface{}.

On the first failure, All5 aborts the sibling tasks and returns zero values for every result together with that error, matching All's fail-fast semantics.

func All6

func All6[T1, T2, T3, T4, T5, T6 any](p1 *Promise[T1], p2 *Promise[T2], p3 *Promise[T3], p4 *Promise[T4], p5 *Promise[T5], p6 *Promise[T6]) (T1, T2, T3, T4, T5, T6, error)

All6 waits for 6 promises of independent types to settle and returns their values as a flat tuple. It is the heterogeneous, fixed-arity analogue of All: join a known number of differently-typed promises without boxing them into interface{}.

On the first failure, All6 aborts the sibling tasks and returns zero values for every result together with that error, matching All's fail-fast semantics.

func All7

func All7[T1, T2, T3, T4, T5, T6, T7 any](p1 *Promise[T1], p2 *Promise[T2], p3 *Promise[T3], p4 *Promise[T4], p5 *Promise[T5], p6 *Promise[T6], p7 *Promise[T7]) (T1, T2, T3, T4, T5, T6, T7, error)

All7 waits for 7 promises of independent types to settle and returns their values as a flat tuple. It is the heterogeneous, fixed-arity analogue of All: join a known number of differently-typed promises without boxing them into interface{}.

On the first failure, All7 aborts the sibling tasks and returns zero values for every result together with that error, matching All's fail-fast semantics.

func All8

func All8[T1, T2, T3, T4, T5, T6, T7, T8 any](p1 *Promise[T1], p2 *Promise[T2], p3 *Promise[T3], p4 *Promise[T4], p5 *Promise[T5], p6 *Promise[T6], p7 *Promise[T7], p8 *Promise[T8]) (T1, T2, T3, T4, T5, T6, T7, T8, error)

All8 waits for 8 promises of independent types to settle and returns their values as a flat tuple. It is the heterogeneous, fixed-arity analogue of All: join a known number of differently-typed promises without boxing them into interface{}.

On the first failure, All8 aborts the sibling tasks and returns zero values for every result together with that error, matching All's fail-fast semantics.

func Any

func Any[T any](ps ...*Promise[T]) (T, error)

Any waits for the first promise to fulfil and returns its value, mirroring JavaScript's Promise.any. As soon as one succeeds, Any aborts the rest.

If every promise fails, Any returns a *AggregateError holding all the reasons in input order. With no promises, Any returns ErrNoPromises.

Example

The JavaScript this mirrors:

const first = await Promise.any([mightFail(), willSucceed()]);
package main

import (
	"errors"
	"fmt"

	"github.com/burrows99/async/promise"
)

func main() {
	first, err := promise.Any(
		promise.New(func() (int, error) { return 0, errors.New("nope") }),
		promise.New(func() (int, error) { return 42, nil }),
	)
	fmt.Println(first, err)
}
Output:
42 <nil>

func Await

func Await[T any](p *Promise[T]) (T, error)

Await blocks until p settles and returns its (value, error). It mirrors the JavaScript `await p` expression positionally — the operator wraps the promise.

func Race

func Race[T any](ps ...*Promise[T]) (T, error)

Race waits for the first promise to settle — success or failure — and returns its result, mirroring JavaScript's Promise.race. Once one promise settles, Race aborts the rest. With no promises, Race returns ErrNoPromises, since Promise.race over an empty array would hang forever.

Types

type AggregateError

type AggregateError struct {
	// Errors holds one entry per input promise, in input order.
	Errors []error
}

AggregateError collects the rejection reasons from every promise, and is what Any returns when they all fail. It mirrors JavaScript's AggregateError, which Promise.any rejects with when no input fulfils.

Its Unwrap returns all the underlying errors, so errors.Is and errors.As match against any of them.

func (*AggregateError) Error

func (e *AggregateError) Error() string

Error implements the error interface.

func (*AggregateError) Unwrap

func (e *AggregateError) Unwrap() []error

Unwrap returns every collected error, enabling errors.Is and errors.As to match against any of them (Go's multi-error unwrapping).

type PanicError

type PanicError struct {
	// Value is the argument that was passed to panic.
	Value any
	// Stack is the goroutine stack trace captured at the moment of recovery.
	Stack []byte
}

PanicError wraps a value recovered from a panic inside work started by New or WithSignal. It is returned from Await in place of the result, turning a panic — which would otherwise crash the process — into an ordinary Go error.

If the recovered value is itself an error, PanicError.Unwrap exposes it, so errors.Is and errors.As see through the PanicError to the original.

Example

A panic inside a task becomes an ordinary error instead of crashing the process — the JavaScript "unhandled rejection is survivable" guarantee.

package main

import (
	"errors"
	"fmt"

	"github.com/burrows99/async/promise"
)

func main() {
	p := promise.New(func() (int, error) {
		panic("something broke")
	})

	_, err := promise.Await(p)
	var pe *promise.PanicError
	if errors.As(err, &pe) {
		fmt.Println("recovered:", pe.Value)
	}
}
Output:
recovered: something broke

func (*PanicError) Error

func (e *PanicError) Error() string

Error implements the error interface.

func (*PanicError) Unwrap

func (e *PanicError) Unwrap() error

Unwrap returns the recovered value if it is itself an error, so errors.Is and errors.As can match against it. It returns nil otherwise.

type Promise

type Promise[T any] struct {
	// contains filtered or unexported fields
}

Promise is a handle to the result of asynchronous work started with New or WithSignal. It is the Go analogue of a JavaScript Promise: a value that becomes available later, together with the error (if any) produced while computing it.

A Promise is safe for concurrent use. The same Promise may be awaited from many goroutines; every caller observes the same settled (value, error) pair.

Cancellation lives inside the Promise, which owns an abort.Controller. Callers never pass a context: they call Promise.Abort, and work that wants to observe cancellation opts in with WithSignal.

func New

func New[T any](fn func() (T, error)) *Promise[T]

New runs fn in a new goroutine and returns a Promise for its result — the Go analogue of calling an async function: `async function f() { ... }` yields a Promise the moment you call it.

fn takes no signal. If fn panics, the panic is recovered and surfaced from Await as a *PanicError; a task New owns never tears down the process. Like a JavaScript async function with no signal wired in, this work cannot be interrupted mid-flight — Promise.Abort marks it aborted for awaiters, but fn runs to completion. When a task must stop on abort, use WithSignal.

Example

The JavaScript this mirrors:

const p = doWork();
const result = await p;
package main

import (
	"fmt"

	"github.com/burrows99/async/promise"
)

func main() {
	p := promise.New(func() (int, error) {
		return 2 + 2, nil
	})
	result, err := promise.Await(p)
	fmt.Println(result, err)
}
Output:
4 <nil>

func Reject added in v0.2.0

func Reject[T any](err error) *Promise[T]

Reject returns a Promise already rejected with err — the analogue of JavaScript's Promise.reject(reason). Await returns the zero value and err.

func Resolve added in v0.2.0

func Resolve[T any](v T) *Promise[T]

Resolve returns a Promise already fulfilled with v — the analogue of JavaScript's Promise.resolve(value). It is handy for starting a chain or for returning a known value where a Promise is expected.

Example

The JavaScript this mirrors:

const v = await Promise.resolve("ready");
package main

import (
	"fmt"

	"github.com/burrows99/async/promise"
)

func main() {
	v, err := promise.Resolve("ready").Await()
	fmt.Println(v, err)
}
Output:
ready <nil>

func Retry

func Retry[T any](fn func() (T, error), opts ...RetryOption) *Promise[T]

Retry runs fn, and if it returns an error, calls it again — up to Attempts times, waiting between tries per the chosen backoff. It returns a Promise that fulfils with the first success, or rejects with the last error if every attempt fails. It is the analogue of npm's p-retry.

The returned Promise is cancellable: aborting it stops the retry loop, and the backoff wait is interrupted at once (an in-flight fn call, which Go cannot preempt, still finishes). By default fn is tried 3 times with no delay; pass ExpBackoff or ConstantBackoff to wait between tries.

Example

The JavaScript this mirrors:

const result = await pRetry(flaky, { retries: 2 });
package main

import (
	"errors"
	"fmt"

	"github.com/burrows99/async/promise"
)

func main() {
	calls := 0
	p := promise.Retry(func() (string, error) {
		calls++
		if calls < 2 {
			return "", errors.New("transient")
		}
		return "ok", nil
	}, promise.Attempts(3))

	result, err := promise.Await(p)
	fmt.Println(result, err)
}
Output:
ok <nil>

func Then added in v0.2.0

func Then[T, U any](p *Promise[T], fn func(T) (U, error)) *Promise[U]

Then transforms a fulfilled Promise's value with fn and returns a new Promise for the result — the analogue of JavaScript's p.then(fn). Because Go methods cannot introduce new type parameters, Then is a package-level function, so chains nest inside-out rather than reading left to right:

// JS:  getUser(id).then(u => u.Name).then(greet)
promise.Then(promise.Then(getUser(id), userName), greet)

If p rejects, Then propagates that error and does not call fn (a then with no rejection handler). fn may itself return an error, which rejects the new Promise. There is deliberately no Catch: handle errors the Go way, from the (T, error) that Await returns.

Example

The JavaScript this mirrors:

const label = await Promise.resolve(21).then(n => n * 2).then(n => `n=${n}`);
package main

import (
	"fmt"

	"github.com/burrows99/async/promise"
)

func main() {
	doubled := promise.Then(promise.Resolve(21), func(n int) (int, error) {
		return n * 2, nil
	})
	labelled := promise.Then(doubled, func(n int) (string, error) {
		return fmt.Sprintf("n=%d", n), nil
	})

	label, err := promise.Await(labelled)
	fmt.Println(label, err)
}
Output:
n=42 <nil>

func Timeout

func Timeout[T any](p *Promise[T], d time.Duration) *Promise[T]

Timeout returns a Promise that settles with p's result, or fails with ErrTimeout if p has not settled after d elapses. It is the analogue of racing work against AbortSignal.timeout(ms).

On timeout the underlying promise is aborted via Promise.Abort, so a task started with WithSignal stops promptly rather than lingering. Because ErrTimeout wraps context.DeadlineExceeded, callers can match either sentinel with errors.Is.

Example

Timeout races work against a deadline, like AbortSignal.timeout(ms).

package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/burrows99/async/abort"
	"github.com/burrows99/async/promise"
)

func main() {
	slow := promise.WithSignal(func(signal *abort.Signal) (int, error) {
		select {
		case <-time.After(time.Hour):
			return 1, nil
		case <-signal.Done():
			return 0, signal.Reason()
		}
	})

	_, err := promise.Await(promise.Timeout(slow, 20*time.Millisecond))
	fmt.Println(errors.Is(err, promise.ErrTimeout))
}
Output:
true

func WithSignal

func WithSignal[T any](fn func(signal *abort.Signal) (T, error)) *Promise[T]

WithSignal is New for work that wants to be cancellable. fn receives an abort.Signal it can observe — the Go analogue of an async function that accepts { signal } and passes it to fetch. When the Promise is aborted (by Promise.Abort, by a combinator cancelling siblings, or by Timeout), the signal fires and a well-behaved fn returns promptly.

func (*Promise[T]) Abort

func (p *Promise[T]) Abort()

Abort marks the Promise aborted — the analogue of AbortController.abort. It fires the abort.Signal handed to a WithSignal task, so a cancellable task stops promptly; a plain New task cannot be interrupted and runs to completion. Abort is idempotent and safe to call from multiple goroutines.

func (*Promise[T]) Await

func (p *Promise[T]) Await() (T, error)

Await blocks until the Promise settles and returns its (value, error). It is the method form of the Await function; both are the Go spelling of `await`. A settled Promise returns immediately; awaiting again re-returns the same result.

func (*Promise[T]) Finally added in v0.2.0

func (p *Promise[T]) Finally(fn func()) *Promise[T]

Finally schedules fn to run once the Promise settles, whatever the outcome, and returns a Promise that carries p's original result through unchanged — the analogue of p.finally(fn). fn takes no arguments and cannot alter the result; it is for cleanup only.

In most Go code a plain defer is clearer and is what the docs recommend; Finally exists for symmetry with the JavaScript API.

func (*Promise[T]) Signal

func (p *Promise[T]) Signal() *abort.Signal

Signal returns the Promise's own abort.Signal, so the same cancellation can be threaded into other cancellable work.

type Result

type Result[T any] struct {
	Value  T
	Reason error
}

Result is the outcome of a single Promise as reported by AllSettled: exactly one of Value (on success) or Reason (on failure) is meaningful. The field names mirror the { value, reason } records returned by JavaScript's Promise.allSettled.

func AllSettled

func AllSettled[T any](ps ...*Promise[T]) []Result[T]

AllSettled waits for every promise to settle and returns one Result per promise, in input order. It never returns early and never fails — the analogue of JavaScript's Promise.allSettled. Each Result carries either a Value (success) or a Reason (failure, including a *PanicError for a panicked task).

func (Result[T]) OK

func (r Result[T]) OK() bool

OK reports whether the Promise settled successfully, that is, Reason == nil.

type RetryOption

type RetryOption func(*retryConfig)

RetryOption configures Retry.

func Attempts

func Attempts(n int) RetryOption

Attempts sets the total number of times Retry calls fn before giving up (the first try counts). Values below 1 are ignored. The default is 3.

func ConstantBackoff

func ConstantBackoff(d time.Duration) RetryOption

ConstantBackoff waits the same duration d between every attempt.

func ExpBackoff

func ExpBackoff(base time.Duration) RetryOption

ExpBackoff waits base after the first failure and doubles the wait after each subsequent one (base, 2·base, 4·base, …) — exponential backoff, as p-retry does by default.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL