Skip to content

Latest commit

 

History

History
161 lines (132 loc) · 4.62 KB

File metadata and controls

161 lines (132 loc) · 4.62 KB
id schema-api-response-error-handling
title Handling Decode Failures
category validating-api-responses
skillLevel beginner
tags
schema
api
error-handling
validation
recovery
lessonOrder 22
rule
description
Handle Decode Failures using Schema.
summary You're decoding API responses with a Schema, but validation sometimes fails. You need to know *why* it failed so you can decide: retry, use a default, log it, or fail explicitly.

Problem

You're decoding API responses with a Schema, but validation sometimes fails. You need to know why it failed so you can decide: retry, use a default, log it, or fail explicitly.

The ParseError gives you detailed info about what went wrong, but you need to handle it gracefully instead of crashing your program.

Solution

import { Effect, Schema, Exit } from "effect"

// Define the schema
const User = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String,
})

type User = typeof User.Type

const parseUser = Schema.decodeUnknown(User)

// Strategy 1: Catch and provide default
const fetchUserWithDefault = (id: number, defaultUser: User) =>
  Effect.gen(function* () {
    const response = yield* Effect.tryPromise(() =>
      fetch(`https://api.example.com/users/${id}`).then((r) => r.json())
    )

    return yield* Effect.orElse(
      parseUser(response),
      () =>
        Effect.gen(function* () {
          yield* Effect.log(`Failed to parse user ${id}, using default`)
          return defaultUser
        })
    )
  })

// Strategy 2: Catch and inspect the error
const fetchUserWithLogging = (id: number) =>
  Effect.gen(function* () {
    const response = yield* Effect.tryPromise(() =>
      fetch(`https://api.example.com/users/${id}`).then((r) => r.json())
    )

    return yield* parseUser(response).pipe(
      Effect.catchTag("ParseError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(`Validation failed: ${error.message}`)
          yield* Effect.log(`Full details: ${JSON.stringify(error.issue)}`)
          return yield* Effect.fail(error)
        })
      )
    )
  })

// Strategy 3: Try multiple decoders (union handling)
const UserV1 = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String,
})

const UserV2 = Schema.Struct({
  userId: Schema.Number,
  fullName: Schema.String,
  contactEmail: Schema.String,
})

const parseUserV1Or2 = (data: unknown) =>
  Effect.gen(function* () {
    // Try V1 first
    const v1Result = yield* Effect.exit(Schema.decodeUnknown(UserV1)(data))

    if (Exit.isSuccess(v1Result)) {
      return v1Result.value
    }

    // Fall back to V2
    const v2Result = yield* Schema.decodeUnknown(UserV2)(data)
    return v2Result
  })

// Strategy 4: Collect all validation errors
const fetchUsersStrict = (ids: number[]) =>
  Effect.gen(function* () {
    const responses = yield* Effect.tryPromise(() =>
      Promise.all(
        ids.map((id) =>
          fetch(`https://api.example.com/users/${id}`).then((r) => r.json())
        )
      )
    )

    // This will fail on first invalid response
    const users = yield* Effect.forEach(
      responses,
      (response) => Schema.decodeUnknown(User)(response)
    )

    yield* Effect.log(`Successfully decoded ${users.length} users`)
    return users
  })

// Handle validation errors
const main = Effect.gen(function* () {
  const defaultUser: User = {
    id: 0,
    name: "Unknown",
    email: "unknown@example.com",
  }

  const user = yield* fetchUserWithDefault(123, defaultUser)
  yield* Effect.log(`Final user: ${user.name}`)
})

Effect.runPromise(main)

Why This Works

Strategy When to Use Tradeoff
orElse API may be degraded but service is important Risk: silently accepting bad data
catchTag Need to understand failure mode Complexity: error inspection logic
Multiple decoders API versioning or polymorphic responses Overhead: multiple parsing attempts
Strict (no catch) API contract is sacred, fail fast is okay Risk: cascading failures if API changes

When to Use

  • API responses may have optional fields
  • Multiple API versions exist simultaneously
  • Graceful degradation is more important than strict correctness
  • You need to log/report validation failures
  • You want to transform errors before returning to caller

Related Patterns