Typed errors
Every policy carries the union of errors it can produce in its type. This union is the error channel. Strategies add errors to it, fallbacks remove them, and functions that return a Result add their own.
import { Tenace } from '@julr/tenace'
const result = await Tenace.policy() .withTimeout('1s') // adds TimeoutError .withCircuitBreaker() // adds CircuitOpenError .withFallbackOn(['CircuitOpenError'], () => null) // removes CircuitOpenError .settle(() => getUser(id))// ^? Result<User | null, UnexpectedError | TimeoutError>Where errors come from
Section titled “Where errors come from”- Thrown values. Anything your function throws becomes an
UnexpectedError. Every channel containsUnexpectedError, because any function can throw. - Returned errors. A function that returns
err(error), or a promise of it, adds the type oferrorto the channel. A function typed to returnunknownoranymay return anyerr(...), so its error channel isunknown. - Strategies. Each strategy declares what it adds and removes. The error reference lists the errors Tenace produces.
Thrown values stay outside the channel on purpose. TypeScript cannot know what a function throws, so a type that claims to know would lie. A function that wants its errors typed returns them.
Define an error
Section titled “Define an error”An error is typed by its _tag, a string literal that tells it apart from the other members of the union. Extend TenaceError, declare _tag and code as literal fields, and return the error with err():
import { err, ok, type Result, Tenace, TenaceError } from '@julr/tenace'
class NotFound extends TenaceError { static readonly tag = 'NotFound' readonly _tag = 'NotFound' as const readonly code = 'E_NOT_FOUND' as const}
declare const db: { find(id: string): Promise<User | undefined> }
async function findUser(id: string): Promise<Result<User, NotFound>> { const user = await db.find(id) return user ? ok(user) : err(new NotFound(`user ${id}`))}
const result = await Tenace.policy() .withTimeout('1s') .settle(() => findUser(id))// ^? Result<User, NotFound | UnexpectedError | TimeoutError>
if (!result.ok && result.error instanceof NotFound) { console.log(result.error.message)}TenaceError gives you:
code, a stable identifier for logs and serialization, such asE_NOT_FOUND.name, set to the class name.toJSON(), which returns the tag, the code, the name, the message, the own fields, and the cause, without the stack trace.- An
instanceofcheck that works across duplicated copies of the package. Withstatic readonly tagset to the value of_tag,instanceof NotFoundalso works across copies of your own class. Without it,instanceofchecks the prototype chain.
isTenaceError(value) returns true for any error created by any copy of Tenace.
Errors do not have to extend TenaceError. Any object with a literal _tag works with retryOn, withFallbackOn(), and match(). Errors without a _tag, such as a string, still flow through the channel, and only the handlers that take every error can handle them: withFallback(), withFallbackWhen(), mapError(), retryIf, and the _ case of match(). withFallbackWhen() handles them but does not remove them from the channel, because a type guard on an untagged class proves nothing about what it matches at runtime.
Match a Result
Section titled “Match a Result”match(result, handlers) calls the ok handler or the handler named after the error _tag, and returns what the handler returns. It is exhaustive. TypeScript reports a missing tag, and a handler for a tag that the result cannot contain.
import { match, Tenace } from '@julr/tenace'
const result = await Tenace.policy() .withTimeout('1s') .settle(() => getUser(id))
const message = match(result, { ok: user => `Hello ${user.name}`, TimeoutError: error => `Timed out after ${error.timeoutMs}ms`, UnexpectedError: error => `Failed: ${String(error.cause)}`,})A _ handler takes every error without its own handler, including untagged errors. It is required when the error type has untagged members, such as strings, plain Errors, or unknown. At runtime, an error with no matching handler and no _ is rethrown. Only untyped input can cause that.
Read the errors of a policy
Section titled “Read the errors of a policy”ErrorOf<typeof policy> is the error channel of a policy, as its callbacks see it:
import { type ErrorOf, Tenace } from '@julr/tenace'
const policy = Tenace.policy().withTimeout('1s').withCircuitBreaker()
type PolicyError = ErrorOf<typeof policy> // UnexpectedError | TimeoutError | CircuitOpenErrorThe final Result type of settle(fn) is always exact for fn. What the callbacks of a reusable policy can see about the errors of fn is the subject of Reusable and bound policies.