Skip to main content

ensurePromise()

ensurePromise<T>(schema, promise): ResultAsync<T, string>

ensurePromise<S>(schema, promise): ResultAsync<Infer<S>, string>

Resolves a Promise, validates the result against a Kanon schema, and returns a ResultAsync. Eliminates the ResultAsync.fromPromise(...).andThen(...) boilerplate when you need to fetch data and validate it in one step.

💎 Why is this a Hidden Gem?

Pass a schema and a promise, get a validated ResultAsync. The shortest path from fetch to typed data.


Type Parameters​

T: T​

The expected output type of the schema.

S: S extends GenericSchema​


Parameters​

Overload 1:

schema: Schema<T>​

Kanon schema to validate against.

promise: Promise<unknown>​

Promise that resolves to the value to validate.

Overload 2:

schema: S​

promise: Promise<unknown>​


Returns: ResultAsync<T, string>​

ResultAsync<T, string> — Ok<T> if the promise resolves and validation succeeds, Err<string> otherwise.


Since​

2.1.0


Example​

import { ensurePromise } from "@pithos/core/bridges/ensurePromise";
import { object, string, number } from "@pithos/core/kanon";

const UserSchema = object({ name: string(), age: number() });

ensurePromise(UserSchema, fetch("/api/user").then(r => r.json()))
.map(user => user.name.toUpperCase());

Use Cases​

Fetch and validate in a single call 📌​

Replace the ResultAsync.fromPromise(...).andThen(validate) boilerplate with one call. Essential for any endpoint that returns JSON you need to trust.

import { ensurePromise } from "@pithos/core/bridges/ensurePromise";
import { string, object, number, optional } from "@pithos/core/kanon";

const productSchema = object({
id: string(),
name: string(),
price: number(),
description: optional(string()),
});

const result = ensurePromise(
productSchema,
fetch("/api/products/42").then(r => r.json()),
);

result
.map(product => ({
...product,
priceFormatted: `${product.price.toFixed(2)}`,
}))
.match(
product => console.log(`${product.name}: ${product.priceFormatted}`),
error => console.error(`Failed: ${error}`),
);

Load remote configuration at startup 📌​

Load and validate a remote JSON config file. Fall back to defaults if the shape is wrong. Perfect for feature flags, A/B testing configs, or any remote settings.

import { ensurePromise } from "@pithos/core/bridges/ensurePromise";
import { string, object, number, boolean, optional } from "@pithos/core/kanon";

const remoteConfigSchema = object({
featureFlags: object({
darkMode: boolean(),
betaAccess: boolean(),
}),
apiVersion: string(),
maxRetries: number(),
maintenanceMessage: optional(string()),
});

async function loadRemoteConfig() {
const config = await ensurePromise(
remoteConfigSchema,
fetch("/config.json").then(r => r.json()),
);

return config.match(
cfg => cfg,
error => {
console.warn(`Remote config invalid (${error}), using defaults`);
return {
featureFlags: { darkMode: false, betaAccess: false },
apiVersion: "v1",
maxRetries: 3,
maintenanceMessage: undefined,
};
},
);
}

Aggregate errors from parallel API calls​

Fetch and validate multiple endpoints in parallel. Collect all errors instead of failing on the first one. Essential for dashboard pages that load data from several sources.

import { ensurePromise } from "@pithos/core/bridges/ensurePromise";
import { string, object, number, array } from "@pithos/core/kanon";
import { ResultAsync } from "@pithos/core/zygos/result/result-async";

const userSchema = object({ id: string(), name: string() });
const statsSchema = object({ views: number(), likes: number() });
const postsSchema = array(object({ id: string(), title: string() }));

function loadDashboard(userId: string) {
return ResultAsync.combineWithAllErrors([
ensurePromise(userSchema, fetch(`/api/users/${userId}`).then(r => r.json())),
ensurePromise(statsSchema, fetch(`/api/stats/${userId}`).then(r => r.json())),
ensurePromise(postsSchema, fetch(`/api/posts?author=${userId}`).then(r => r.json())),
]).map(([user, stats, posts]) => ({
user,
stats,
posts,
}));
}

// ResultAsync<Dashboard, string[]> - all errors collected if multiple fail

Upload a file and validate the server response​

Upload a file and validate what the server sends back in one pipeline. Perfect for file upload flows where the response shape matters.

import { ensurePromise } from "@pithos/core/bridges/ensurePromise";
import { string, object, number } from "@pithos/core/kanon";

const uploadResponseSchema = object({
fileId: string(),
url: string(),
size: number(),
});

function uploadFile(file: File) {
const body = new FormData();
body.append("file", file);

return ensurePromise(
uploadResponseSchema,
fetch("/api/upload", { method: "POST", body }).then(r => r.json()),
).map(response => ({
...response,
sizeFormatted: `${(response.size / 1024).toFixed(1)} KB`,
}));
}

// ResultAsync<{ fileId, url, size, sizeFormatted }, string>

Load design system tokens from a remote source​

Fetch and validate design tokens from a remote theme server. Essential for design systems that load theme configurations dynamically.

import { ensurePromise } from "@pithos/core/bridges/ensurePromise";
import { string, object, number, array } from "@pithos/core/kanon";

const themeTokensSchema = object({
colors: object({
primary: string(),
secondary: string(),
background: string(),
text: string(),
}),
spacing: object({
unit: number(),
scale: array(number()),
}),
breakpoints: object({
sm: number(),
md: number(),
lg: number(),
xl: number(),
}),
});

function loadTheme(themeId: string) {
return ensurePromise(
themeTokensSchema,
fetch(`/api/themes/${themeId}`).then(r => r.json()),
).match(
tokens => applyThemeTokens(tokens),
error => {
console.warn(`Theme ${themeId} invalid: ${error}, using defaults`);
applyThemeTokens(defaultTokens);
},
);
}

Trust a GraphQL response​

GraphQL servers can return unexpected shapes. Validate the response before using it. Critical for typed frontends that rely on specific response structures.

import { ensurePromise } from "@pithos/core/bridges/ensurePromise";
import { string, object, number } from "@pithos/core/kanon";

const gqlResponseSchema = object({
data: object({
repository: object({
name: string(),
stargazerCount: number(),
issues: object({
totalCount: number(),
}),
}),
}),
});

function fetchRepoInfo(owner: string, name: string) {
const query = `query ($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) { name stargazerCount issues { totalCount } }
}`;

return ensurePromise(
gqlResponseSchema,
fetch("https://api.github.com/graphql", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({ query, variables: { owner, name } }),
}).then(r => r.json()),
).map(res => res.data.repository);
}

// ResultAsync<{ name, stargazerCount, issues: { totalCount } }, string>