🚀 TypeScript: Async and Error Types
The Pizza Delivery Story 🍕
Imagine you order a pizza. You don’t stand at the door waiting the whole time—you do other things while the pizza is being made and delivered. When the doorbell rings, you get your pizza (success!) or maybe the driver says “Sorry, we ran out of cheese” (error!).
This is exactly how async code works in TypeScript. Let’s learn how to tell TypeScript what kind of pizza (data) we’re expecting, and what to do when things go wrong!
🎯 Promise Typing
What is a Promise?
A Promise is like a pizza order receipt. It says: “I promise to give you something later.”
// A promise that will give us a string
const myPromise: Promise<string> =
new Promise((resolve) => {
resolve("Hello!");
});
The Magic Formula
Promise<WhatYouWillGet>
Simple Example:
Promise<string>→ You’ll get textPromise<number>→ You’ll get a numberPromise<User>→ You’ll get a User object
Real Example: Fetching a User
// Define what a User looks like
interface User {
id: number;
name: string;
email: string;
}
// This function PROMISES to give us a User
async function getUser(): Promise<User> {
const user: User = {
id: 1,
name: "Sam",
email: "sam@example.com"
};
return user;
}
Why Type Promises?
Without typing:
// ❌ TypeScript doesn't know what's inside
async function getUser() {
return someData; // What is this???
}
With typing:
// ✅ Crystal clear!
async function getUser(): Promise<User> {
return userData; // Must be a User!
}
graph TD A["📝 Call async function"] --> B["🎫 Get a Promise"] B --> C{⏳ Waiting...} C --> D["✅ resolve: Get your data!"] C --> E["❌ reject: Get an error!"]
🛡️ Error Handling Types
The Problem: Errors are Sneaky
In JavaScript, anything can go wrong. TypeScript helps us catch problems!
Custom Error Types
Create your own error types to be super specific:
// Define specific error types
class NetworkError extends Error {
constructor(
message: string,
public statusCode: number
) {
super(message);
this.name = "NetworkError";
}
}
class ValidationError extends Error {
constructor(
message: string,
public field: string
) {
super(message);
this.name = "ValidationError";
}
}
Using Error Types
function validateEmail(email: string): void {
if (!email.includes("@")) {
throw new ValidationError(
"Invalid email",
"email"
);
}
}
try {
validateEmail("bad-email");
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Fix the ${error.field} field`);
}
}
The Result Pattern 🎁
Instead of throwing errors, return a “result box” that either has your data OR an error:
// Success or Failure - clearly typed!
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function divide(
a: number,
b: number
): Result<number> {
if (b === 0) {
return {
success: false,
error: new Error("Can't divide by zero!")
};
}
return { success: true, data: a / b };
}
// Using it
const result = divide(10, 2);
if (result.success) {
console.log(result.data); // 5 ✅
} else {
console.log(result.error.message); // ❌
}
graph TD A["🎯 Function runs"] --> B{Did it work?} B -->|Yes| C["✅ {success: true, data: ...}"] B -->|No| D["❌ {success: false, error: ...}"]
🌐 Typing API Responses
The Real World
When you call an API, you get JSON back. TypeScript needs to know what shape that JSON has!
Step 1: Define Your Types
// What the API returns
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface Product {
id: number;
name: string;
price: number;
}
Step 2: Type Your Fetch
async function fetchProducts():
Promise<ApiResponse<Product[]>> {
const response = await fetch("/api/products");
const json = await response.json();
// Now TypeScript knows json is ApiResponse<Product[]>
return json as ApiResponse<Product[]>;
}
Step 3: Handle All Cases
interface ApiError {
code: string;
message: string;
}
type ApiResult<T> =
| { ok: true; data: ApiResponse<T> }
| { ok: false; error: ApiError };
async function safeApiFetch<T>(
url: string
): Promise<ApiResult<T>> {
try {
const response = await fetch(url);
if (!response.ok) {
return {
ok: false,
error: {
code: String(response.status),
message: "Request failed"
}
};
}
const data = await response.json();
return { ok: true, data };
} catch (e) {
return {
ok: false,
error: {
code: "NETWORK_ERROR",
message: "Could not connect"
}
};
}
}
Using the Safe Fetch
const result = await safeApiFetch<Product[]>(
"/api/products"
);
if (result.ok) {
// TypeScript knows: result.data exists
console.log(result.data.data); // Product[]
} else {
// TypeScript knows: result.error exists
console.log(result.error.message);
}
✨ The using Keyword (NEW!)
What is using?
The using keyword is like a responsible friend who always cleans up after a party! 🎉
When you’re done with something (like a file or database connection), using makes sure it gets closed properly.
The Old Way (Messy 😰)
async function readFile() {
const file = await openFile("data.txt");
try {
const content = await file.read();
return content;
} finally {
await file.close(); // Don't forget!
}
}
The New Way (Clean 😊)
async function readFile() {
await using file = await openFile("data.txt");
const content = await file.read();
return content;
// file.close() happens automatically! ✨
}
How to Make It Work
Your object needs a special cleanup method:
// For sync cleanup
interface Disposable {
[Symbol.dispose](): void;
}
// For async cleanup
interface AsyncDisposable {
[Symbol.asyncDispose](): Promise<void>;
}
Real Example: Database Connection
class DatabaseConnection
implements AsyncDisposable {
private connected = false;
async connect() {
console.log("🔌 Connecting...");
this.connected = true;
}
async query(sql: string) {
return `Results for: ${sql}`;
}
// This runs automatically when done!
async [Symbol.asyncDispose]() {
console.log("👋 Disconnecting...");
this.connected = false;
}
}
// Usage
async function getData() {
await using db = new DatabaseConnection();
await db.connect();
const data = await db.query("SELECT *");
return data;
// 👋 Disconnecting... happens automatically!
}
When to Use using
graph TD A["Need to use a resource?"] --> B{Does it need cleanup?} B -->|Yes| C["Use &#39;using&#39; keyword!"] B -->|No| D["Regular variable is fine"] C --> E["File handles"] C --> F["Database connections"] C --> G["Network sockets"] C --> H["Locks and semaphores"]
🎯 Quick Summary
| Concept | What It Does | Key Syntax |
|---|---|---|
| Promise Typing | Tells what a promise returns | Promise<User> |
| Error Types | Custom errors with extra info | class MyError extends Error |
| Result Pattern | Success or error, never throw | Result<T, E> |
| API Response Types | Shape of API data | ApiResponse<T> |
using keyword |
Auto-cleanup resources | using x = ... |
🧠 Remember This!
- Always type your Promises → Know what you’re getting
- Create custom error classes → Be specific about what went wrong
- Use the Result pattern → Never let errors surprise you
- Type your API responses → Trust the data you receive
- Use
usingfor cleanup → Never forget to close things
You’ve got this! TypeScript’s type system is your friend, making sure your async code is safe and predictable. 🚀
