Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions apps/web/content/blog/getting-started-with-deessejs-errors.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
title: "Getting Started with @deessejs/errors"
description: "Learn how to implement Python-inspired error handling in TypeScript with exception chaining, hierarchical inheritance, and rich error semantics."
author: "Nesalia Inc"
date: "2026-06-05"
---

## Introduction

`@deessejs/errors` brings Python's powerful error handling system to TypeScript. If you've ever wished JavaScript had the same expressive error handling as Python, this library is for you.

## Installation

```bash
npm install @deessejs/errors
```

## Creating Your First Error

Unlike traditional JavaScript errors that use classes, `@deessejs/errors` uses a function-based API:

```ts
import { error } from '@deessejs/errors';

// Create an error factory
const ValidationError = error({
name: 'ValidationError',
message: 'Validation failed for field "{field}"',
});

// Use it
const err = ValidationError({ field: 'email' });
console.log(err.message); // "Validation failed for field "email""
```

## Exception Chaining

The `.from()` method lets you chain errors together, preserving the full context of what went wrong:

```ts
import { error, raise } from '@deessejs/errors';

const AppError = error({ name: 'AppError' });
const ValidationError = error({ name: 'ValidationError' });

try {
// Something fails
raise(ValidationError({ field: 'email' }));
} catch (e) {
// Chain the error
AppError({}).from(e);
}
```

## Hierarchical Inheritance

Build error hierarchies that let you catch errors at different levels:

```ts
import { error, is } from '@deessejs/errors';

const AppError = error({ name: 'AppError' });
const ValidationError = error({
name: 'ValidationError',
inherits: AppError,
});

// Now you can catch any AppError
if (is(err, AppError)) {
// Handles both ValidationError and AppError
}
```

## Next Steps

- Read the [Error Factory documentation](/docs/error-factory) to learn about message templates
- Explore [Exception Chaining](/docs/from-method) for advanced error handling
- Check out [Recipes](/docs/recipes) for common patterns

Happy error handling!
12 changes: 11 additions & 1 deletion apps/web/source.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
import { defineConfig, defineDocs, defineCollections } from 'fumadocs-mdx/config';
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
import { z } from 'zod';

// You can customize Zod schemas for frontmatter and `meta.json` here
// see https://fumadocs.dev/docs/mdx/collections
Expand All @@ -16,6 +17,15 @@ export const docs = defineDocs({
},
});

export const blog = defineCollections({
type: 'doc',
dir: 'content/blog',
schema: pageSchema.extend({
author: z.string(),
date: z.iso.date().or(z.date()),
}),
});

export default defineConfig({
mdxOptions: {
// MDX options
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/app/(home)/blog/[slug]/page.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';
import { Check, Share } from 'lucide-react';
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';

export function ShareButton({ url }: { url: string }) {
const [isChecked, onCopy] = useCopyButton(() => {
void navigator.clipboard.writeText(`${window.location.origin}${url}`);
});

return (
<button
type="button"
className="inline-flex items-center justify-center gap-2 rounded-md border bg-background px-3 py-1.5 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
onClick={onCopy}
>
{isChecked ? <Check className="size-4" /> : <Share className="size-4" />}
{isChecked ? 'Copied!' : 'Share'}
</button>
);
}
129 changes: 129 additions & 0 deletions apps/web/src/app/(home)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { InlineTOC } from 'fumadocs-ui/components/inline-toc';
import { blogSource } from '@/lib/source';
import { baseUrl } from '@/lib/shared';
import { getMDXComponents } from '@/components/mdx';
import { ShareButton } from './page.client';

export default async function Page(props: PageProps<'/blog/[slug]'>) {
const params = await props.params;
const page = blogSource.getPage([params.slug]);

if (!page) notFound();

const MDX = page.data.body;
const toc = page.data.toc;

return (
<article className="flex flex-col mx-auto w-full max-w-[800px] px-4 py-8">
{/* Author& Date */}
<div className="flex flex-row gap-8 text-sm mb-8">
<div>
<p className="mb-1 text-fd-muted-foreground">Written by</p>
<p className="font-medium">{page.data.author}</p>
</div>
<div>
<p className="mb-1 text-fd-muted-foreground">Published</p>
<p className="font-medium">
{new Date(page.data.date).toDateString()}
</p>
</div>
</div>

{/* Title & Description */}
<h1 className="text-3xl font-semibold mb-4">{page.data.title}</h1>
<p className="text-fd-muted-foreground mb-8">{page.data.description}</p>

{/* Actions */}
<div className="flex flex-row gap-2 mb-8">
<ShareButton url={page.url} />
<Link
href="/blog"
className="px-3 py-1.5 text-sm border rounded-md hover:bg-fd-accent"
>
Back to Blog
</Link>
</div>

{/* Content */}
<div className="prose min-w-0 flex-1">
<InlineTOC items={toc} />
<MDX components={getMDXComponents()} />
</div>

{/* Article JSON-LD */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: page.data.title,
description: page.data.description,
datePublished: page.data.date,
dateModified: page.data.date,
author: {
'@type': 'Person',
name: page.data.author,
},
publisher: {
'@type': 'Organization',
name: 'Nesalia Inc',
logo: {
'@type': 'ImageObject',
url: `${baseUrl}/icon.svg`,
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `${baseUrl}${page.url}`,
},
}).replace(/</g, '\\u003c'),
}}
/>
</article>
);
}

export async function generateMetadata(props: PageProps<'/blog/[slug]'>): Promise<Metadata> {
const params = await props.params;
const page = blogSource.getPage([params.slug]);

if (!page) notFound();

return {
title: page.data.title,
description: page.data.description,
alternates: {
canonical: `${baseUrl}${page.url}`,
},
openGraph: {
title: page.data.title,
description: page.data.description,
type: 'article',
publishedTime: new Date(page.data.date).toISOString(),
authors: [page.data.author],
images: [
{
url: `${baseUrl}/og/blog/${params.slug}/image.png`,
width: 1200,
height: 630,
alt: page.data.title,
},
],
},
twitter: {
card: 'summary_large_image',
title: page.data.title,
description: page.data.description,
},
};
}

export function generateStaticParams(): { slug: string }[] {
return blogSource.getPages().map((page) => ({
slug: page.slugs[0],
}));
}
61 changes: 61 additions & 0 deletions apps/web/src/app/(home)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Link from 'next/link';
import { blogSource } from '@/lib/source';
import { baseUrl } from '@/lib/shared';
import type { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Blog',
description: 'Latest articles about TypeScript error handling, exception chaining, and best practices with @deessejs/errors.',
alternates: {
canonical: `${baseUrl}/blog`,
},
};

function getName(path: string) {
return path.split('/').pop()?.replace(/\.[^.]+$/, '') ?? path;
}

export default function Page() {
const posts = [...blogSource.getPages()].sort(
(a, b) =>
new Date(b.data.date ?? getName(b.path)).getTime() -
new Date(a.data.date ?? getName(a.path)).getTime(),
);

return (
<main className="mx-auto w-full max-w-page px-4 pb-12 md:py-12">
{/* Hero Section */}
<div className="relative mb-8">
<h1 className="text-3xl font-semibold mb-2">@deessejs/errors Blog</h1>
<p className="text-fd-muted-foreground">
Latest articles about TypeScript error handling, exception chaining, and best practices.
</p>
</div>

{/* Post Grid */}
{posts.length === 0 ? (
<div className="text-center py-12 text-fd-muted-foreground">
<p>No blog posts yet. Check back soon!</p>
</div>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<Link
key={post.url}
href={post.url}
className="flex flex-col bg-fd-card rounded-xl border p-4 transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
>
<h2 className="font-medium mb-2 line-clamp-2">{post.data.title}</h2>
<p className="text-sm text-fd-muted-foreground mb-4 line-clamp-2">
{post.data.description}
</p>
<time className="text-xs text-fd-muted-foreground mt-auto">
{new Date(post.data.date ?? getName(post.path)).toDateString()}
</time>
</Link>
))}
</div>
)}
</main>
);
}
36 changes: 36 additions & 0 deletions apps/web/src/app/(home)/blog/rss.xml/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Feed } from 'feed';
import { blogSource } from '@/lib/source';
import { NextResponse } from 'next/server';
import { baseUrl } from '@/lib/shared';

export const revalidate = false;

export function GET() {
const feed = new Feed({
title: '@deessejs/errors Blog',
id: `${baseUrl}/blog`,
link: `${baseUrl}/blog`,
language: 'en',
description: 'Latest articles about TypeScript error handling, exception chaining, and best practices.',
image: `${baseUrl}/banner.jpg`,
favicon: `${baseUrl}/icon.svg`,
copyright: `All rights reserved ${new Date().getFullYear()}, Nesalia Inc`,
});

for (const page of blogSource.getPages().sort((a, b) => {
return new Date(b.data.date).getTime() - new Date(a.data.date).getTime();
})) {
feed.addItem({
id: page.url,
title: page.data.title,
description: page.data.description,
link: `${baseUrl}${page.url}`,
date: new Date(page.data.date),
author: [{ name: page.data.author }],
});
}

return new NextResponse(feed.rss2(), {
headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' },
});
}
9 changes: 8 additions & 1 deletion apps/web/src/app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';

export default function Layout({ children }: LayoutProps<'/docs'>) {
const base = baseOptions();

return (
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
<DocsLayout
tree={source.getPageTree()}
{...base}
// Only show icon-type links in sidebar (filter out text links like Blog)
links={base.links?.filter((item) => item.type === 'icon')}
>
{children}
</DocsLayout>
);
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export default function Layout({ children }: LayoutProps<'/'>) {
<html lang="en" className={inter.className} suppressHydrationWarning>
<head>
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
<link
rel="alternate"
type="application/rss+xml"
title="@deessejs/errors Blog"
href="/blog/rss.xml"
/>
<JsonLd />
</head>
<body className="flex flex-col min-h-screen">
Expand Down
Loading
Loading