Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add blog to website #132

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
58 changes: 58 additions & 0 deletions components/BlogIndex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getPagesUnderRoute } from "nextra/context"
import * as React from "react"
import Link from "next/link"

export declare namespace BlogIndex {
export interface Props {
readonly more?: string
readonly pagesUnder: string
}
}

export const BlogIndex: React.FC<BlogIndex.Props> = ({
more = "Read more",
pagesUnder,
}) => (
<React.Fragment>
{getPagesUnderRoute(pagesUnder)
.filter((page) => page.kind === "MdxPage")
.map((page) => {
const meta = "meta" in page ? page.meta : undefined
const frontMatter = "frontMatter" in page ? page.frontMatter : undefined
return (
<div key={page.route} className="mb-10">
<h3>
<Link
href={page.route}
style={{ color: "inherit", textDecoration: "none" }}
className="block font-semibold mt-8 text-2xl "
>
{frontMatter?.series && (
<span className="align-middle text-xs nx-bg-primary-100 nx-font-semibold nx-text-primary-800 dark:nx-bg-primary-100/10 dark:nx-text-primary-600 inline-block py-1 px-2 uppercase rounded mb-1 mr-2">
Series{" "}
</span>
)}
<span>{meta?.title || frontMatter?.title || page.name}</span>
</Link>
</h3>
<p className="opacity-80 mt-6 leading-7">
{frontMatter?.description}{" "}
<span className="inline-block">
<Link
href={page.route}
className="text-[color:hsl(var(--nextra-primary-hue),100%,50%)] underline underline-offset-2 decoration-from-font"
>
{more + " →"}
</Link>
</span>
</p>
{frontMatter?.date ? (
<p className="opacity-50 text-sm mt-6 leading-7">
{frontMatter.date}
</p>
) : null}
</div>
)
})}
</React.Fragment>
)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"clean": "rm -rf .next node_modules/.cache"
},
"dependencies": {
"@effect/schema": "^0.25.0",
"@stackblitz/sdk": "^1.9.0",
"next": "13.3.1",
"nextra": "^2.7.1",
Expand Down
4 changes: 4 additions & 0 deletions pages/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@
"theme": {
"layout": "full"
}
},
"blog": {
"title": "Blog",
"type": "page"
}
}
12 changes: 12 additions & 0 deletions pages/blog.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: Blog
searchable: false
---

<h1 className="text-4xl tracking-tighter text-center font-extrabold md:text-5xl mt-8 pb-6">
Effect Blog
</h1>

import { BlogIndex } from "@/components/BlogIndex"

<BlogIndex pagesUnder="/blog" />
9 changes: 9 additions & 0 deletions pages/blog/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"*": {
"theme": {
"pagination": false,
"sidebar": false
}
},
"type-safe-errors": "Type-Safe Errors in TypeScript"
}
10 changes: 10 additions & 0 deletions pages/blog/type-safe-errors.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: Blog
description: 'In this article series we will explore a set of alternative options to represent errors in a type-safe way'
searchable: false
series: true
---

import { BlogIndex } from "@/components/BlogIndex"

<BlogIndex pagesUnder="/blog/type-safe-errors" />
8 changes: 8 additions & 0 deletions pages/blog/type-safe-errors/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"*": {
"theme": {
"pagination": true
}
},
"naive-approach": "A naive approach"
}
163 changes: 163 additions & 0 deletions pages/blog/type-safe-errors/naive-approach.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
description: 'In this article we will take a look at the usual way we represent errors in TypeScript'
date: June 7th, 2023
---

# A naive approach using classes with instanceof

The first step to achieve type safety in error management as one may imagine is to define errors, let's have a look at a usual piece of code with the objective of improving its safety.

```ts
import * as S from "@effect/schema/Schema"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion:

Given that we explicitly recommend fully qualified name-spaced imports in the documentation, I would suggest being explicit in the blog as well. I.E:

import * as Schema from "@effect/schema/Schema"


const Todo_ = S.struct({
id: S.number,
userId: S.number,
title: S.string,
completed: S.boolean,
})
export interface Todo extends S.To<typeof Todo_> {}
export const Todo: S.Schema<Todo> = Todo_

export const fetchTodo = (id: number) =>
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
.then((response) => response.json())
.then((value) => S.parseSync(Todo)(value))
```

Why can this code fail? when looking at the implementation we can easily spot at least 6 different causes of failure:

1) network errors
2) status not 200/404
3) malformed input, non integer
4) todo can't be found (response 400)
5) malformed response, non json
6) malformed todo

By looking at the type signature we have:

```ts
const fetchTodo: (id: number) => Promise<Todo>
```

As we may imagine this isn't that helful in conveying the above information and we also have no way of really distinguishing between the failure cases unless we do some very deep investigation on what every single function we call can throw.

The very first step would be to at least isolate the potential failure scenarios in a way that we can at least identify them separately.

```ts
class NetworkError {
readonly _tag = "NetworkError"
constructor(readonly error: unknown) {}
}

class ResponseStatusError {
readonly _tag = "ResponseStatusError"
constructor(readonly response: Response) {}
}

class MalformedResponse {
readonly _tag = "MalformedResponse"
constructor(readonly error: unknown) {}
}

class MalformedTodo {
readonly _tag = "MalformedTodo"
constructor(readonly error: unknown) {}
}

class MalformedId {
readonly _tag = "MalformedId"
constructor(readonly id: number) {}
}

class TodoNotFound {
readonly _tag = "TodoNotFound"
constructor(readonly id: number) {}
}
```

rewriting the code above and trying to use async-await leads to quick explosion, nesting try-catch blocks to identify every possible case looks horrible. We can get a decent result if we introduce a function like:

```ts
export const tryOrError = <T, E>(
f: () => T,
onErr: (error: unknown) => E
): T => {
try {
const r = f()
if (typeof r === "object" && r !== null && "then" in r) {
// @ts-expect-error
return (r as any as Promise<Awaited<T>>).catch((e) => {
throw onErr(e)
})
}
return r
} catch (e) {
throw onErr(e)
}
}
```

that wraps a generic function (or Promise-returning function) in a try-catch block isolating its error.

Let's see how it would look like:


```ts
export const fetchTodo = async (id: number) => {
if (!Number.isInteger(id)) {
throw new MalformedId(id)
}
const response = await tryOrError(
() => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`),
(error) => new NetworkError(error)
)
if (response.status === 404) {
throw new TodoNotFound(id)
}
if (response.status !== 200) {
throw new ResponseStatusError(response)
}
const json = await tryOrError(
() => response.json(),
(error) => new MalformedResponse(error)
)
return tryOrError(
() => S.parseSync(Todo)(json),
(error) => new MalformedTodo(error)
)
}
```

While the type hasn't changed we are now able to isolate the individual causes of failure, for example we may write code like the following:

```ts
const main = async () => {
try {
const todos = await Promise.all([1, 2, 3, 4].map(fetchTodo))
console.log(`todos:`)
for (const todo of todos) {
console.log(`---`)
console.log(`id: ${todo.id}`)
console.log(`userId: ${todo.userId}`)
console.log(`title: ${todo.title}`)
console.log(`completed: ${todo.completed}`)
console.log(`---`)
}
} catch (error) {
if (error instanceof TodoNotFound) {
console.error(`Todo with ${error.id} not found`)
} else {
console.error(`Unexpected error occurred`, error)
}
}
}
```

Here we are explicitely dealing with a Todo that was not found differently from the other scenarios, this is a classic example of a very important distinction.

Not all errors are equal for our program, some errors are to be accounted for in our domain and some are not, like in this case a todo that was mistakenly retrieved by our consumer can be considered a predictable scenario to be hendled with a helpful message while a malformed response would instead indicate a defect of the system that should never occur in normal scenarios.

Even if TS was smart enough to know all the error types that may occur in when doing `catch (error)` we would still be left with poor ability to disciminate between domain errors and defects and more importantly we can't have guarantees that our program won't fail for other reasons (such as a stack limit reached) so we effectively the type of `error` would always be `unknown | something else`, potentially with `something else` being a very long list of errors.

Can we do better? yes, and that's what we will explore in the next article of the series where we will introduce the concept of result types.
32 changes: 26 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading