| |

TypeScript 63 🔷 Promises and Async/Await Typing

A Promise is a value that will be available later. An async function returns a Promise, and await unwraps the Promise’s value. The JavaScript runtime has had these for years, and TypeScript’s types for them are precise: Promise<T> is the type of a Promise that resolves to T, async functions return Promise<T> automatically, and await narrows the type from Promise<T> to T. But the typing has subtleties that are easy to miss: the Promise<T> return type is not what the async function’s body returns, the rejection path is untyped, the Awaited<T> utility unwraps nested Promises, and the error handling requires a type guard because the caught value is unknown. This chapter covers the typing of Promises and async/await in detail: how the types flow, how the errors are handled, how the Awaited<T> and Promise.all and Promise.allSettled work, and the patterns that make async code type-safe.

Key point: An async function always returns a Promise<T>, where T is the type of the value the function returns. The await expression unwraps the Promise and produces the resolved value’s type. The Promise<T> type is generic, and the T is the resolved type. The Awaited<T> utility recursively unwraps the Promise types, which is why Awaited<Promise<Promise<number>>> is number. The rejection path is untyped — the catch clause receives unknown in strict mode, and a type guard is required to narrow it. The Promise.all returns Promise<T[]>, the Promise.allSettled returns Promise<PromiseSettledResult<T>[]>, and the Promise.race returns Promise<T>. The try/catch/finally blocks handle the resolution and the rejection, and the finally block runs on both paths.


The Promise<T> type

A Promise is a generic type. The T is the type of the value the Promise resolves to.

const promise: Promise<number> = Promise.resolve(42);
const value: number = await promise;

The Promise<number> is the type of a Promise that resolves to a number. The await unwraps it to the number.

Why the T is the resolved type. The Promise<T> describes the value that is available when the Promise settles. The T is the value’s type, and the await is the operation that accesses it.

Why the Promise is a container. A Promise is a box that will contain a value. The Promise<T> is the type of the box, and the T is the type of what is inside. The await opens the box.

Why the Promise can resolve to void. A Promise that resolves to nothing has the type Promise<void>.

const saved: Promise<void> = save();
await saved;  // no value

The void is the type of the nothing, and the await produces the void.

Why the Promise can resolve to never. A Promise that never resolves has the type Promise<never>.

const infinite: Promise<never> = new Promise(() => {});

The never is the type of the never, and the Promise is the type of the box that never opens.

Why the Promise can be rejected. A Promise that rejects has the rejection value, which is not part of the Promise<T> type. The T is the resolution type, and the rejection is the separate channel that is untyped in the type system.

Why the type is not the same as the value. The Promise<number> is the type of the Promise, and the 42 is the value it resolves to. The two are the box and the content, and the await is the operation that connects them.


async functions

An async function always returns a Promise<T>, where T is the type of the value the function returns. The compiler infers the return type, and the explicit annotation is optional.

async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json() as Promise<User>;
}

The function returns a Promise<User>, and the return statement returns the User. The compiler wraps the returned value in a Promise automatically.

Why the async function returns a Promise. The async keyword makes the function’s return value a Promise. The function’s body is the Promise’s executor, and the return is the resolution.

Why the explicit return type is recommended. The explicit Promise<User> documents the function’s contract and catches the mistakes. The inferred type is the same, but the explicit annotation is the clarity.

Why the return inside the async function is not the Promise. The return response.json() returns the Promise from response.json(), and the async function wraps it again. The result is a Promise<Promise<User>> that flattens to Promise<User>. The flattening is automatic.

async function getUser(id: string): Promise<User> {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

The return returns a Promise<User>, and the async function flattens it. The two forms are the same.

Why the async function can return a value directly. The return value inside the async function resolves the Promise with the value. The two are the same, and the return await is not needed.

Why the return await is sometimes needed. The return await is needed inside a try/catch block, because the await makes the error catchable. The return promise inside the try does not catch the rejection, and the return await promise does.

async function safe(): Promise<User> {
  try {
    return await fetchUser();  // the rejection is caught
  } catch (error) {
    return fallbackUser;
  }
}

Why the async function’s errors are rejections. A throw inside an async function rejects the returned Promise. The throw and the rejected Promise are the same, and the catch clause handles both.

async function risky(): Promise<void> {
  throw new Error('failed');  // the Promise is rejected
}

risky().catch((e) => console.error(e));

Why the async function can be used without await. The function returns a Promise, and the caller can use it with .then or with await. The two are the same, and the choice is the style.


The await expression

The await expression unwraps a Promise. Its type is the resolved type of the Promise.

const promise: Promise<number> = Promise.resolve(42);
const value = await promise;  // number

The await is the unwrapping, and the value is the number. The compiler infers the type from the Promise’s T.

Why the await can be used with a non-Promise. The await accepts a non-Promise value and returns it unchanged.

const value = await 42;  // 42

The await on a non-Promise is a no-op, and the value is the same. The behavior is the convenience.

Why the await unwraps the nested Promises. The await recursively unwraps the nested Promises, which is why await Promise.resolve(Promise.resolve(42)) is 42.

const value = await Promise.resolve(Promise.resolve(42));  // 42

The recursive unwrapping is the Awaited<T> behavior, and the await is the runtime operation.

Why the await must be inside an async function. The await is allowed only inside an async function (or at the top level of a module in modern environments). The compiler rejects the await in a non-async function.

Why the top-level await is allowed in modules. The modern module system allows the top-level await, and the module’s evaluation is asynchronous. The await at the top level is the modern feature, and it is used for the initialization.

// In a module
const config = await loadConfig();
export default config;

Why the await in a loop is a performance concern. The await in a loop serializes the operations, which is slow for the independent operations. The Promise.all is the parallel alternative.

// Slow: sequential
for (const id of ids) {
  const user = await getUser(id);
  users.push(user);
}

// Fast: parallel
const users = await Promise.all(ids.map((id) => getUser(id)));

The parallel version is the performance, and the sequential version is the correctness when the order matters.

Why the await inside a map is a common mistake. The map with an async callback returns an array of Promises, not a Promise of an array. The Promise.all is the fix.

// Wrong: the result is Promise<User>[]
const users = ids.map(async (id) => getUser(id));

// Right: the result is User[]
const users = await Promise.all(ids.map((id) => getUser(id)));

The mistake is the type mismatch, and the fix is the Promise.all.


The error typing

The rejection path is untyped in the Promise’s type. The catch clause receives unknown in strict mode, and a type guard is required to narrow it.

try {
  const user = await getUser(id);
} catch (error) {
  // error is `unknown`
  if (error instanceof Error) {
    console.error(error.message);  // narrowed to Error
  } else {
    console.error('Unknown error', error);
  }
}

The error is unknown, and the instanceof Error guard narrows it to the Error type.

Why the error is unknown. The JavaScript runtime can throw anything — a string, a number, an object, an Error. The unknown is the correct type, and it forces the guard. The any would be the wrong type, and it would not force the guard.

Why the useUnknownInCatchVariables option matters. The useUnknownInCatchVariables: true option (implied by strict) makes the catch variable unknown. The false makes it any. The true is the safe default.

Why the type guard is necessary. The unknown cannot be used without a narrowing. The instanceof Error is the common guard, and the custom guards handle the other types.

function isApiError(error: unknown): error is ApiError {
  return error instanceof ApiError;
}

try {
  await api.call();
} catch (error) {
  if (isApiError(error)) {
    console.error(error.status);  // narrowed to ApiError
  }
}

The custom guard is the type predicate, and the error is ApiError is the narrowing.

Why the instanceof Error is not always enough. A rejection from a library may be a plain object, a string, or an HttpErrorResponse. The instanceof Error catches the Error types, and the other types need their own guards.

Why the Promise’s rejection type is not in the type system. The Promise<T> type describes the resolution, and the rejection is the separate channel. The type system does not track the rejection’s type, and the guard is the runtime check. The Result<T, E> type is the alternative that tracks both.

Why the Result<T, E> pattern is the alternative. A function that returns a Result<T, E> tracks the error type in the type system, and the caller handles both branches.

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

async function getUser(id: string): Promise<Result<User, ApiError>> {
  try {
    const user = await fetchUser(id);
    return { ok: true, value: user };
  } catch (error) {
    return { ok: false, error: error as ApiError };
  }
}

The Result tracks the error, and the caller narrows on the ok field. The pattern is the alternative to the exception, and the two are the choice.

Why the Result pattern is not the default. The exceptions are the JavaScript convention, and the Result requires the discipline. The two are the choice, and the exceptions are the default.


The Awaited<T> utility

The Awaited<T> utility recursively unwraps the Promise types. It is the type-level version of the await.

type A = Awaited<Promise<number>>;            // number
type B = Awaited<Promise<Promise<number>>>;   // number
type C = Awaited<number>;                     // number
type D = Awaited<Promise<string> | number>;   // string | number

The Awaited<T> is the recursive unwrapping, and the await is the runtime operation. The two are the same, and the utility is the type-level version.

Why the Awaited<T> is useful. The utility unwraps the nested Promises in the type-level code. A generic function that accepts a Promise<T> and returns the resolved type uses the Awaited<T>.

async function unwrap<T>(promise: Promise<T>): Promise<Awaited<T>> {
  return await promise;
}

The Awaited<T> is the recursive unwrap, and the return type is the resolved type.

Why the Awaited<T> is the standard utility. The TypeScript 4.5 added the Awaited<T>, and the earlier versions used the PromiseValue or the conditional types. The Awaited<T> is the standard, and the code should use it.

Why the Awaited<T> handles the thenable. The Awaited<T> works with the thenables — the objects with a then method — as well as the Promises. The utility is the general unwrap.

Why the Awaited<T> is the type of the await. The await expression’s type is the Awaited<T> of the operand. The two are the same, and the utility is the way to express it in the type-level code.

Why the Awaited<T> is the recursive. The Awaited<T> unwraps the nested Promises, which the single Promise<T> does not. The recursive unwrap is the await‘s behavior, and the utility is the type-level version.


The Promise combinators

The Promise.all, Promise.allSettled, Promise.race, and Promise.any have the typed signatures. Each has a specific result type.

The Promise.all. The Promise.all takes an array of Promises and returns a Promise of the array of the resolved values. It rejects if any of the Promises rejects.

const [user, items, settings] = await Promise.all([
  getUser(),
  getItems(),
  getSettings(),
]);
// user: User, items: Item[], settings: Settings

The tuple’s types are inferred from the input’s Promises. The three Promises are the tuple, and the result is the tuple of the resolved values.

Why the Promise.all is the parallel operation. The Promises are started at the same time, and the result is the array of the values. The parallel is the performance, and the rejection is the all-or-nothing.

The Promise.allSettled. The Promise.allSettled takes an array of Promises and returns a Promise of the array of the settled results. It never rejects.

const results = await Promise.allSettled([
  getUser(),
  getItems(),
]);
// results: PromiseSettledResult<User | Item[]>[]
for (const result of results) {
  if (result.status === 'fulfilled') {
    console.log(result.value);
  } else {
    console.log(result.reason);
  }
}

The PromiseSettledResult<T> is the discriminated union of the fulfilled and the rejected. The status is the discriminant, and the value and the reason are the payloads.

Why the Promise.allSettled is the resilient operation. The operation never rejects, and the results are the per-Promise statuses. The caller handles the failures individually, and the successful results are the values.

The Promise.race. The Promise.race takes an array of Promises and returns the first one to settle. The result is the type of the first settled Promise.

const result = await Promise.race([
  fetchWithTimeout(url),
  timeout(5000),
]);

The Promise.race is the type of the union of the Promises’ types, and the first settled wins.

Why the Promise.race is the timeout pattern. The race between the operation and the timeout is the timeout pattern, and the first settled is the result. The pattern is the common use.

The Promise.any. The Promise.any takes an array of Promises and returns the first one to resolve. It rejects only if all the Promises reject.

const result = await Promise.any([
  fetchFromMirror1(),
  fetchFromMirror2(),
]);

The Promise.any is the first success, and the AggregateError is the rejection when all the Promises fail.

Why the Promise.any is the fallback pattern. The multiple mirrors are the fallbacks, and the first success is the result. The pattern is the resilience, and the AggregateError is the all-failed case.

Why the combinators’ types are precise. Each combinator’s return type is the specific shape, and the Promise.allSettled‘s PromiseSettledResult<T> is the discriminated union. The types are the modern TypeScript, and the code uses them.


Complete Example Session

// ============================================
// PART 1: THE PROMISE TYPE
// ============================================

const promise: Promise<number> = Promise.resolve(42);
const value: number = await promise;

// ============================================
// PART 2: THE ASYNC FUNCTION
// ============================================

interface User { id: string; name: string; }

async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json() as Promise<User>;
}

// ============================================
// PART 3: THE AWAIT
// ============================================

const user = await getUser('1');
// user is User

// ============================================
// PART 4: THE ERROR TYPING
// ============================================

try {
  const user = await getUser('1');
} catch (error) {
  // error is unknown
  if (error instanceof Error) {
    console.error(error.message);
  }
}

// ============================================
// PART 5: THE CUSTOM GUARD
// ============================================

class ApiError extends Error {
  constructor(readonly status: number) {
    super(`API error ${status}`);
  }
}

function isApiError(error: unknown): error is ApiError {
  return error instanceof ApiError;
}

try {
  await getUser('1');
} catch (error) {
  if (isApiError(error)) {
    console.error(error.status);
  }
}

// ============================================
// PART 6: THE AWAITED UTILITY
// ============================================

type A = Awaited<Promise<number>>;            // number
type B = Awaited<Promise<Promise<number>>>;   // number
type C = Awaited<number>;                     // number

// ============================================
// PART 7: THE PROMISE.ALL
// ============================================

const [u, items, settings] = await Promise.all([
  getUser('1'),
  getItems(),
  getSettings(),
]);
// u: User, items: Item[], settings: Settings

// ============================================
// PART 8: THE PROMISE.ALLSETTLED
// ============================================

const results = await Promise.allSettled([
  getUser('1'),
  getItems(),
]);

for (const result of results) {
  if (result.status === 'fulfilled') {
    console.log(result.value);
  } else {
    console.log(result.reason);
  }
}

// ============================================
// PART 9: THE RESULT PATTERN
// ============================================

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

async function safeGetUser(id: string): Promise<Result<User, ApiError>> {
  try {
    const user = await getUser(id);
    return { ok: true, value: user };
  } catch (error) {
    return { ok: false, error: error as ApiError };
  }
}

const result = await safeGetUser('1');
if (result.ok) {
  console.log(result.value.name);
} else {
  console.log(result.error.status);
}

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't use `await` in a `map` callback without Promise.all
const users = ids.map(async (id) => getUser(id));  // Promise<User>[]

// Don't assume the caught error is an Error
catch (error) {
  console.error(error.message);  // error is unknown
}

// Don't use the `Promise.all` for the independent operations that can fail
// Use the `Promise.allSettled`.

// Don't forget the `return await` inside the `try` block
// The rejection is not caught without it.

// Don't use the `any` for the Promise's type
const p: Promise<any> = ...;  // loses the type

// Don't forget the `Awaited<T>` for the generic unwrap

The ten parts cover the Promise type, the async function, the await, the error typing, the custom guard, the Awaited, the Promise.all, the Promise.allSettled, the Result pattern, and the anti-patterns.


Quick Reference

The Types

TypeMeaning
Promise<T>A Promise resolving to T
Promise<void>A Promise resolving to nothing
Promise<never>A Promise that never resolves
Awaited<T>The recursive unwrap
PromiseSettledResult<T>The settled result

The Async Function

FormReturn Type
async function f(): Promise<T>Promise<T>
return valueResolves with the value
throw errorRejects with the error
return await promiseCatches the rejection in the try

The Error Typing

AspectValue
The catch variableunknown (strict)
The guardinstanceof Error
The custom guarderror is T
The optionuseUnknownInCatchVariables

The Combinators

CombinatorResult
Promise.allPromise<T[]> (rejects on any)
Promise.allSettledPromise<PromiseSettledResult<T>[]>
Promise.racePromise<T> (first settled)
Promise.anyPromise<T> (first resolved)

The Awaited<T>

InputOutput
Promise<number>number
Promise<Promise<number>>number
numbernumber
Promise<string> | numberstring | number

Best Practices

✅ Do This:

// Use the explicit Promise return type
async function getUser(id: string): Promise<User> { ... }      // ✅

// Use the `return await` inside the try
try { return await fetchUser(); } catch { ... }                // ✅

// Guard the caught error
catch (error) {
  if (error instanceof Error) console.error(error.message);
}                                                              // ✅

// Use the custom guard for the custom errors
function isApiError(e: unknown): e is ApiError { ... }         // ✅

// Use the `Promise.all` for the parallel operations
const [a, b] = await Promise.all([getA(), getB()]);            // ✅

// Use the `Promise.allSettled` for the resilient operations
const results = await Promise.allSettled([...]);               // ✅

// Use the `Awaited<T>` for the generic unwrap
type Resolved<T> = Awaited<T>;                                 // ✅

// Use the `Result<T, E>` for the tracked errors
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; // ✅

❌ Don’t Do This:

// Don't use the `await` in a `map` without the `Promise.all`
const users = ids.map(async (id) => getUser(id));  // Promise<User>[] // ⚠️

// Don't assume the caught error is an Error
catch (error) { console.error(error.message); }  // unknown       // ⚠️

// Don't use the `Promise.all` for the independent operations that can fail
await Promise.all([...]);  // one rejection cancels the rest     // ⚠️

// Don't forget the `return await` in the try
try { return fetchUser(); } catch { }  // the rejection escapes  // ⚠️

// Don't use the `any` for the Promise's type
const p: Promise<any> = ...;                                   // ⚠️

// Don't use the `Promise` for the synchronous code
async function add(a: number, b: number): Promise<number> { return a + b; } // ⚠️

// Don't forget the top-level await's module requirement
const config = await loadConfig();  // needs the module context  // ⚠️

Common Pitfalls

PitfallProblemSolution
await in mapPromise<T>[]Use Promise.all
Caught error assumedunknownGuard with instanceof
Promise.all for independentOne rejection cancelsUse allSettled
return promise in tryThe rejection escapesUse return await
any PromiseThe type is lostUse the generic
Async for sync codeUnnecessary overheadUse the sync function
Missing top-level moduleThe await is rejectedUse the module context

Real-World Examples

1. The basic Promise

const promise: Promise<number> = Promise.resolve(42);

2. The async function

async function getUser(id: string): Promise<User> { ... }

3. The await

const user = await getUser('1');

4. The error guard

catch (error) {
  if (error instanceof Error) console.error(error.message);
}

5. The custom guard

function isApiError(e: unknown): e is ApiError { return e instanceof ApiError; }

6. The Promise.all

const [a, b] = await Promise.all([getA(), getB()]);

7. The Promise.allSettled

const results = await Promise.allSettled([getA(), getB()]);

8. The Awaited

type Resolved = Awaited<Promise<number>>;  // number

9. The Result pattern

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

10. The return await in the try

try { return await fetchUser(); } catch { return fallback; }

Visual: The Promise Type

┌──────────────────────────────────────────────────────────┐
│  Promise<number>                                         │
│    │                                                     │
│    │  the box                                            │
│    │                                                     │
│    └── resolves to a number                              │
│                                                          │
│  const p: Promise<number> = Promise.resolve(42);         │
│  const value: number = await p;                          │
│                                                          │
│  The `T` is the resolved type.                           │
│  The `await` unwraps it.                                 │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Async Function

┌──────────────────────────────────────────────────────────┐
│  async function getUser(id: string): Promise<User> {     │
│    const response = await fetch(`/api/users/${id}`);     │
│    return response.json() as Promise<User>;              │
│  }                                                       │
│                                                          │
│  The `async` keyword wraps the return in a Promise.      │
│  The `return` value is the User.                         │
│  The function returns `Promise<User>`.                   │
│                                                          │
│  The caller:                                             │
│    const user = await getUser('1');  // User             │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Error Typing

┌──────────────────────────────────────────────────────────┐
│  try {                                                   │
│    const user = await getUser('1');                      │
│  } catch (error) {                                       │
│    // error is `unknown`                                 │
│    if (error instanceof Error) {                         │
│      console.error(error.message);  // narrowed          │
│    } else {                                              │
│      console.error('Unknown', error);                    │
│    }                                                     │
│  }                                                       │
│                                                          │
│  The catch variable is `unknown` in strict mode.         │
│  The guard narrows it.                                   │
│  The `any` would not force the guard.                    │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Combinators

┌──────────────────────────────────────────────────────────┐
│  Promise.all([a, b, c])                                  │
│    → Promise<[A, B, C]>                                  │
│    → rejects if any rejects                              │
│                                                          │
│  Promise.allSettled([a, b, c])                           │
│    → Promise<PromiseSettledResult<A | B | C>[]>          │
│    → never rejects                                       │
│                                                          │
│  Promise.race([a, b, c])                                 │
│    → Promise<A | B | C>                                  │
│    → the first to settle                                 │
│                                                          │
│  Promise.any([a, b, c])                                  │
│    → Promise<A | B | C>                                  │
│    → the first to resolve                                │
│    → rejects if all reject                               │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Awaited Utility

┌──────────────────────────────────────────────────────────┐
│  Awaited<Promise<number>>                                │
│    → number                                              │
│                                                          │
│  Awaited<Promise<Promise<number>>>                       │
│    → number  (recursive)                                 │
│                                                          │
│  Awaited<number>                                         │
│    → number                                              │
│                                                          │
│  Awaited<Promise<string> | number>                       │
│    → string | number                                     │
│                                                          │
│  The `Awaited<T>` is the type-level `await`.             │
│  It recursively unwraps the nested Promises.             │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

ItemValue
Promise typePromise<T>
Async returnPromise<T>
Await resultAwaited<T>
Catch variableunknown (strict)
Guardinstanceof Error
Custom guarderror is T
Promise.allPromise<T[]>
Promise.allSettledPromise<PromiseSettledResult<T>[]>
Awaited<T>The recursive unwrap
Result<T, E>The tracked error pattern

Key takeaways:

  • An async function always returns a Promise<T> — the return value is the T, and the compiler wraps it in a Promise automatically
  • The await unwraps the Promise to the Awaited<T> — the unwrapping is recursive, and the await Promise.resolve(Promise.resolve(42)) is 42
  • The await must be inside an async function or at the top level of a module — the top-level await is the modern feature, and it is used for the initialization
  • The caught error is unknown in strict mode — the instanceof Error guard narrows it, and the custom guard handles the custom error types
  • The Promise.all is the parallel operation that rejects on any failure — the Promise.allSettled is the resilient alternative that never rejects
  • The Promise.allSettled‘s result is the PromiseSettledResult<T> discriminated union — the status field distinguishes the fulfilled from the rejected
  • The Awaited<T> utility is the type-level await — it recursively unwraps the nested Promises, and it is the standard utility for the generic code
  • The Result<T, E> pattern tracks the error type in the type system — it is the alternative to the exceptions, and the caller narrows on the ok field
  • The await in a map without the Promise.all is a common mistake — the map returns the array of Promises, and the Promise.all is the fix
  • The return await inside a try block is needed to catch the rejection — the return promise escapes the catch, and the return await promise does not

Remember: The Promises and the async/await are the asynchronous foundation, and TypeScript’s types for them are precise. The Promise<T> is the box, the async function returns the box, the await opens the box, and the Awaited<T> is the type-level version. The rejection path is untyped, and the guard is the runtime check. The combinators — all, allSettled, race, any — have the specific result types, and the Result<T, E> is the alternative for the tracked errors. The types are the map, and the async code is the territory.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!