0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

effect-tsでlayerで複数個所のエラーをまとめて対応する方法

Last updated at Posted at 2025-09-27

モックのOKとNGなどはテストのファイルで用意する気がしていたが
いろいろとeffect-tsのドキュメントを読むうちにソースのファイルのほうで用意する気がしてきた

モックのOKとNGを両方用意して、それですべてのパターンを簡単にテストできるようになる
これでプログラム構造を確定させてから、モックでない処理を書いていくような気がする

前回

エラー対応追加したもの

get_data_from_domain で Effect.catchTags を使用して
複数個所で発生するエラーを1つで対応している

これによって
OK時の動作をget_data_from_domainの前方で記述し、
NG時の動作をget_data_from_domainの後方で記述するだけでよい

下位のエラーもすべて対応できそう

ngのモックの時にLayer.succeed を使うのが、なんか微妙な感じがする

import { Cache, Context, Data, Duration, Effect, Layer } from "effect"

type type_employee_id = string
type type_department_id = string

class DatabaseError extends Data.TaggedError("DatabaseError")<{
  table_name: string
}> {}

class DomainError extends Data.TaggedError("DomainError")<{
  reason: string
}> {}

// ---- db : employee ----

interface interface_employee {
  employee_id: type_employee_id
  employee_name: string
  department_id: type_department_id
}

class database_employee extends Context.Tag("database_employee")<
  database_employee,
  {
    readonly get_one_employee: (
      employee_id: type_employee_id
    ) => Effect.Effect<interface_employee, DatabaseError>
  }
>() {}

const database_employee_mock_ok = Layer.succeed(
  database_employee,
  {
    get_one_employee: (employee_id: type_employee_id) =>
      Effect.gen(function*() {
        yield* Effect.log("get_one_employee ok start")
        yield* Effect.sleep(Duration.seconds(5))
        const data: interface_employee = {
          employee_id,
          employee_name: "test employee",
          department_id: test_department_id
        }
        yield* Effect.log("get_one_employee end")
        return data
      })
  }
)

const database_employee_mock_ng_database_error = Layer.succeed(
  database_employee,
  {
    get_one_employee: (_: type_employee_id) =>
      Effect.gen(function*() {
        yield* Effect.log("get_one_employee ng start")
        return yield* Effect.fail(new DatabaseError({ table_name: "employee" }))
      })
  }
)

const employee_cache = Cache.make({
  capacity: 100,
  timeToLive: Duration.seconds(5),
  lookup: (employee_id: type_employee_id) =>
    Effect.gen(function*() {
      yield* Effect.log("employee_cache lookup start")
      const DatabaseEmployee = yield* database_employee
      const employee = yield* DatabaseEmployee.get_one_employee(employee_id)
      return employee
    })
})

// ---- db : department ----

interface interface_department {
  department_id: type_department_id
  department_name: string
}

class database_department extends Context.Tag("database_department")<
  database_department,
  {
    readonly get_one_department: (
      department_id: type_department_id
    ) => Effect.Effect<interface_department, DatabaseError>
  }
>() {}

const database_department_mock_ok = Layer.succeed(
  database_department,
  {
    get_one_department: (department_id: type_department_id) =>
      Effect.gen(function*() {
        yield* Effect.log("get_one_department ok start")
        yield* Effect.sleep(Duration.seconds(3))
        const data: interface_department = {
          department_id,
          department_name: "test department"
        }
        yield* Effect.log("get_one_department end")
        return data
      })
  }
)

const database_department_mock_ng_database_error = Layer.succeed(
  database_department,
  {
    get_one_department: (_: type_department_id) =>
      Effect.gen(function*() {
        yield* Effect.log("get_one_department ng start")
        return yield* Effect.fail(new DatabaseError({ table_name: "department" }))
      })
  }
)

const department_cache = Cache.make({
  capacity: 100,
  timeToLive: Duration.seconds(5),
  lookup: (department_id: type_department_id) =>
    Effect.gen(function*() {
      yield* Effect.log("department_cache lookup start")
      const DatabaseDepartment = yield* database_department
      const department = yield* DatabaseDepartment.get_one_department(department_id)
      return department
    })
})

// ---- test data ----

const test_employee_id: type_employee_id = "e_00_test"
const test_department_id: type_department_id = "d_00_test"

// ---- domain -----------------

interface employee_data {
  employee: interface_employee
  department: interface_department
}

class domain_employee extends Context.Tag("domain_employee")<
  domain_employee,
  {
    readonly get_one_employee_data: (
      employee_id: type_employee_id
    ) => Effect.Effect<employee_data, DatabaseError | DomainError>
  }
>() {}

const domain_employee_service = Layer.effect(
  domain_employee,
  Effect.gen(function*() {
    yield* Effect.log("domain_employee_service start")
    const CacheEmployee = yield* employee_cache
    const CacheDepartment = yield* department_cache
    return {
      get_one_employee_data: (
        employee_id: type_employee_id
      ) =>
        Effect.gen(function*() {
          yield* Effect.log("get_one_employee_data start")
          const isHitEmployee = yield* CacheEmployee.contains(employee_id)
          if (isHitEmployee) yield* Effect.log("employee_data from cache")
          const employee = yield* CacheEmployee.get(employee_id)

          const isHitDepartment = yield* CacheDepartment.contains(employee.department_id)
          if (isHitDepartment) yield* Effect.log("department_data from cache")
          const department = yield* CacheDepartment.get(employee.department_id)
          const result: employee_data = {
            employee,
            department
          }
          return result
        })
    }
  })
)

const domain_employee_service_ng = Layer.effect(
  domain_employee,
  Effect.gen(function*() {
    yield* Effect.log("domain_employee_service_ng start")
    return {
      get_one_employee_data: (
        _: type_employee_id
      ) =>
        Effect.gen(function*() {
          yield* Effect.log("get_one_employee_data start")
          return yield* Effect.fail(new DomainError({ reason: "原因不明のエラーが発生" }))
        })
    }
  })
)

const get_data_from_domain = Effect.gen(function*() {
  yield* Effect.log("get_data_from_domain start")
  const DomainEmployee = yield* domain_employee
  const result_employee = yield* DomainEmployee.get_one_employee_data(test_employee_id)
  yield* Effect.log(result_employee)
  const result_employee_2 = yield* DomainEmployee.get_one_employee_data(test_employee_id)
  yield* Effect.log(result_employee_2)
  yield* Effect.sleep(Duration.seconds(5))
  const result_employee_3 = yield* DomainEmployee.get_one_employee_data(test_employee_id)
  yield* Effect.log(result_employee_3)
}).pipe(
  Effect.catchTags({
    DatabaseError: (error) => Effect.logError(`DatabaseErrorが発生しました : ${error.table_name}`),
    DomainError: (error) => Effect.logError(`DomainErrorが発生しました : ${error.reason}`)
  })
)

// ---- layer ----

const domain_service_ok = Effect.provide(get_data_from_domain, domain_employee_service)
const domain_service_ng = Effect.provide(get_data_from_domain, domain_employee_service_ng)

const service_mock_ok = Layer.mergeAll(database_employee_mock_ok, database_department_mock_ok)
const service_mock_ng_employee = Layer.mergeAll(
  database_employee_mock_ng_database_error,
  database_department_mock_ok
)
const service_mock_ng_department = Layer.mergeAll(
  database_employee_mock_ok,
  database_department_mock_ng_database_error
)

// ---- program ----

const program_domain_mock_ok = domain_service_ok.pipe(Effect.provide(service_mock_ok))
const program_domain_mock_ng = domain_service_ng.pipe(Effect.provide(service_mock_ok))
const program_domain_mock_ng_employee = domain_service_ok.pipe(Effect.provide(service_mock_ng_employee))
const program_domain_mock_ng_department = domain_service_ok.pipe(Effect.provide(service_mock_ng_department))

// ---- Entry Point ----

Effect.runPromise(program_domain_mock_ok).catch((reason) => {
  console.log(reason)
})

OK時

最後を以下にする

Effect.runPromise(program_domain_mock_ok).catch((reason) => {
  console.log(reason)
})

実行結果

timestamp=2025-09-27T13:43:46.004Z level=INFO fiber=#0 message="domain_employee_service start"
timestamp=2025-09-27T13:43:46.008Z level=INFO fiber=#0 message="get_data_from_domain start"
timestamp=2025-09-27T13:43:46.009Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-09-27T13:43:46.012Z level=INFO fiber=#0 message="employee_cache lookup start"
timestamp=2025-09-27T13:43:46.013Z level=INFO fiber=#0 message="get_one_employee ok start"
timestamp=2025-09-27T13:43:51.023Z level=INFO fiber=#0 message="get_one_employee end"
timestamp=2025-09-27T13:43:51.030Z level=INFO fiber=#0 message="department_cache lookup start"
timestamp=2025-09-27T13:43:51.033Z level=INFO fiber=#0 message="get_one_department ok start"
timestamp=2025-09-27T13:43:54.044Z level=INFO fiber=#0 message="get_one_department end"
timestamp=2025-09-27T13:43:54.045Z level=INFO fiber=#0 message="{
  \"employee\": {
    \"employee_id\": \"e_00_test\",
    \"employee_name\": \"test employee\",
    \"department_id\": \"d_00_test\"
  },
  \"department\": {
    \"department_id\": \"d_00_test\",
    \"department_name\": \"test department\"
  }
}"
timestamp=2025-09-27T13:43:54.046Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-09-27T13:43:54.046Z level=INFO fiber=#0 message="employee_data from cache"
timestamp=2025-09-27T13:43:54.047Z level=INFO fiber=#0 message="department_data from cache"
timestamp=2025-09-27T13:43:54.048Z level=INFO fiber=#0 message="{
  \"employee\": {
    \"employee_id\": \"e_00_test\",
    \"employee_name\": \"test employee\",
    \"department_id\": \"d_00_test\"
  },
  \"department\": {
    \"department_id\": \"d_00_test\",
    \"department_name\": \"test department\"
  }
}"
timestamp=2025-09-27T13:43:59.052Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-09-27T13:43:59.055Z level=INFO fiber=#0 message="employee_data from cache"
timestamp=2025-09-27T13:43:59.059Z level=INFO fiber=#0 message="employee_cache lookup start"
timestamp=2025-09-27T13:43:59.061Z level=INFO fiber=#0 message="get_one_employee ok start"
timestamp=2025-09-27T13:44:04.070Z level=INFO fiber=#0 message="get_one_employee end"
timestamp=2025-09-27T13:44:04.073Z level=INFO fiber=#0 message="department_data from cache"
timestamp=2025-09-27T13:44:04.076Z level=INFO fiber=#0 message="department_cache lookup start"
timestamp=2025-09-27T13:44:04.079Z level=INFO fiber=#0 message="get_one_department ok start"
timestamp=2025-09-27T13:44:07.085Z level=INFO fiber=#0 message="get_one_department end"
timestamp=2025-09-27T13:44:07.085Z level=INFO fiber=#0 message="{
  \"employee\": {
    \"employee_id\": \"e_00_test\",
    \"employee_name\": \"test employee\",
    \"department_id\": \"d_00_test\"
  },
  \"department\": {
    \"department_id\": \"d_00_test\",
    \"department_name\": \"test department\"
  }
}"

メイン処理で NG時

最後を以下にする

Effect.runPromise(program_domain_mock_ng).catch((reason) => {
  console.log(reason)
})

実行結果

timestamp=2025-09-27T13:59:10.172Z level=INFO fiber=#0 message="domain_employee_service_ng start"
timestamp=2025-09-27T13:59:10.178Z level=INFO fiber=#0 message="get_data_from_domain start"
timestamp=2025-09-27T13:59:10.179Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-09-27T13:59:10.181Z level=ERROR fiber=#0 message="DomainErrorが発生しました : 原因不明のエラーが発生"

データベース(employee)で NG時

最後を以下にする

Effect.runPromise(program_domain_mock_ng_employee).catch((reason) => {
  console.log(reason)
})

実行結果

timestamp=2025-09-27T14:00:05.968Z level=INFO fiber=#0 message="domain_employee_service start"
timestamp=2025-09-27T14:00:05.976Z level=INFO fiber=#0 message="get_data_from_domain start"
timestamp=2025-09-27T14:00:05.977Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-09-27T14:00:05.980Z level=INFO fiber=#0 message="employee_cache lookup start"
timestamp=2025-09-27T14:00:05.981Z level=INFO fiber=#0 message="get_one_employee ng start"
timestamp=2025-09-27T14:00:05.984Z level=ERROR fiber=#0 message="DatabaseErrorが発生しました : employee"

データベース(department)で NG時

最後を以下にする

Effect.runPromise(program_domain_mock_ng_employee).catch((reason) => {
  console.log(reason)
})

実行結果

timestamp=2025-09-27T14:00:41.821Z level=INFO fiber=#0 message="domain_employee_service start"
timestamp=2025-09-27T14:00:41.826Z level=INFO fiber=#0 message="get_data_from_domain start"
timestamp=2025-09-27T14:00:41.827Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-09-27T14:00:41.829Z level=INFO fiber=#0 message="employee_cache lookup start"
timestamp=2025-09-27T14:00:41.831Z level=INFO fiber=#0 message="get_one_employee ok start"
timestamp=2025-09-27T14:00:46.834Z level=INFO fiber=#0 message="get_one_employee end"
timestamp=2025-09-27T14:00:46.835Z level=INFO fiber=#0 message="department_cache lookup start"
timestamp=2025-09-27T14:00:46.836Z level=INFO fiber=#0 message="get_one_department ng start"
timestamp=2025-09-27T14:00:46.838Z level=ERROR fiber=#0 message="DatabaseErrorが発生しました : department"
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?