A Task<T, E> is a type representing the state of an asynchronous computation which may fail, with a successful value of type T or an error of type E. It has three states:
Pending
Resolved, with a value of type T
Rejected, with a reason of type E
In general, however, because of the asynchronous nature of a Task, you will interact with it via its methods, rather than matching on its state, since you generally want to perform an operation once it has resolved.
You can think of a Task<T, E> as being basically a Promise<Result<T, E>>, because it is a Promise<Result<T, E>> under the hood, but with two main differences:
A Task cannot reject. All rejections must be handled. This means that, like a Result, it will never throw an error if used in strict TypeScript.
Unlike Promise, Task robustly distinguishes between map and andThen operations.
Task also implements JavaScript’s PromiseLike interface, so you canawait it; when a Task<T, E> is awaited, it produces a Result<T, E>.
Creating a Task
The simplest way to create a Task is to call Task.try(somePromise). Because any promise may reject/throw an error, this simplest form catches all rejections and maps them into the Rejected variant. Given a Promise<T>, the resulting Task thus has the type Task<T, unknown>. For example:
let { promise, reject } = Promise.withResolvers<number>();
// `theTask` has the type `Task<number, unknown>` lettheTask = Task.try(promise);
// The rejection will always produce reject("Tasks always safely handle errors!"); awaittheTask; console.log(theTask.state); // State.Rejected
// The `reason` here is of type `unknown`. Attempting to access it on a pending // or resolved `Task` (rather than a rejected `Task`) will throw an error. console.log(theTask.reason); // "Tasks always safely handle errors!"
You can also provide a fallback value for the error using tryOr:
let { promise, reject } = Promise.withResolvers<number>();
// `theTask` has the type `Task<number, string>` lettheTask = Task.tryOr(promise, "a fallback error");
reject({ thisStructuredObject:"will be ignored!" }); awaittheTask;
console.log(theTask.reason); // "a fallback error"
You can use Task.tryOrElse to produce a known rejection reason from the unknown rejection reason of a Promise:
let { promise, reject } = Promise.withResolvers<number>();
// `theTask` has the type `Task<number, Error>` lettheTask = Task.tryOrElse( promise, (reason) =>newError("Promise was rejected", { cause:reason }) );
Task also has resolved and rejected static helpers:
// `resolved` has the type `Task<number, never>` letresolved = Task.resolved(123);
// `rejected` has the type `Task<never, string>` letrejected = Task.rejected("something went wrong");
Working with a Task
There are many helpers (“combinators”) for working with a Task. The most common are map, mapRejected, andThen, and orElse.
andThen uses the value produced by one resolved Task to create another Task, but without nesting them. orElse is like andThen, but for the Rejection. You can often combine them to good effect. For example, a safe fetch usage might look like this:
Because Task wraps Promise, it (currently) always requires at least two ticks of the microtask queue before it will produce its final state. In practical terms, you must alwaysawait a Task before its state will be Resolved or Rejected, even with Task.resolved and Task.rejected. If the (Stage 1) Faster Promise Adoption TC39 proposal is adopted, this may change/improve.
Task
A
Task<T, E>
is a type representing the state of an asynchronous computation which may fail, with a successful value of typeT
or an error of typeE
. It has three states:Pending
Resolved
, with a value of typeT
Rejected
, with a reason of typeE
In general, however, because of the asynchronous nature of a
Task
, you will interact with it via its methods, rather than matching on its state, since you generally want to perform an operation once it has resolved.You can think of a
Task<T, E>
as being basically aPromise<Result<T, E>>
, because it is aPromise<Result<T, E>>
under the hood, but with two main differences:A
Task
cannot reject. All rejections must be handled. This means that, like aResult
, it will never throw an error if used in strict TypeScript.Unlike
Promise
,Task
robustly distinguishes betweenmap
andandThen
operations.Task
also implements JavaScript’sPromiseLike
interface, so you canawait
it; when aTask<T, E>
is awaited, it produces aResult<T, E>
.Creating a
Task
The simplest way to create a
Task
is to callTask.try(somePromise)
. Because any promise may reject/throw an error, this simplest form catches all rejections and maps them into theRejected
variant. Given aPromise<T>
, the resultingTask
thus has the typeTask<T, unknown>
. For example:You can also provide a fallback value for the error using
tryOr
:You can use
Task.tryOrElse
to produce a known rejection reason from theunknown
rejection reason of aPromise
:Task
also hasresolved
andrejected
static helpers:Working with a
Task
There are many helpers (“combinators”) for working with a
Task
. The most common aremap
,mapRejected
,andThen
, andorElse
.map
transforms a value “within” aTask
context:mapRejected
does the same, but for a rejection:andThen
uses the value produced by one resolvedTask
to create anotherTask
, but without nesting them.orElse
is likeandThen
, but for theRejection
. You can often combine them to good effect. For example, a safefetch
usage might look like this:There are many others; see the API docs!
Timing
Because
Task
wrapsPromise
, it (currently) always requires at least two ticks of the microtask queue before it will produce its final state. In practical terms, you must alwaysawait
aTask
before itsstate
will beResolved
orRejected
, even withTask.resolved
andTask.rejected
. If the (Stage 1) Faster Promise Adoption TC39 proposal is adopted, this may change/improve.