Error Handling
Testcontainers OCaml uses a combination of Lwt-based error handling and a custom exception type for container-related errors.
Error Types
All Testcontainers errors are wrapped in the Testcontainers_error exception:
exception Testcontainers_error of Error.t
The Error.t type covers all possible failure scenarios:
type t =
| Container_not_found of string
| Container_not_running of string
| Container_start_failed of { id : string; message : string }
| Container_stop_failed of { id : string; message : string }
| Wait_timeout of { strategy : string; timeout : float }
| Docker_error of { status : int; message : string }
| Docker_connection_failed of string
| Invalid_configuration of string
| Image_pull_failed of { image : string; message : string }
| Port_not_mapped of { container_port : int; protocol : string }
Converting Errors to Strings
let handle_error err =
let message = Error.to_string err in
Printf.printf "Error: %s\n" message
Example outputs:
Container not found: abc123def456
Failed to start container abc123: port already allocated
Wait strategy 'port:5432/tcp' timed out after 60.0s
Docker API error (status 404): No such image: invalid:latest
Port 8080/tcp not mapped
Catching Errors
Basic Pattern
open Lwt.Syntax
let run_test () =
Lwt.catch
(fun () ->
Container.with_container request (fun container ->
(* test code *)
Lwt.return_unit
))
(function
| Error.Testcontainers_error err ->
Printf.printf "Testcontainers error: %s\n" (Error.to_string err);
Lwt.return_unit
| exn ->
Printf.printf "Other error: %s\n" (Printexc.to_string exn);
Lwt.return_unit)
Handling Specific Errors
let run_with_retry () =
Lwt.catch
(fun () -> Container.start request)
(function
| Error.Testcontainers_error (Error.Image_pull_failed { image; message }) ->
Printf.printf "Failed to pull %s: %s\n" image message;
Printf.printf "Trying with local image...\n";
let local_request = Container_request.with_image "local:latest" request in
Container.start local_request
| Error.Testcontainers_error (Error.Wait_timeout { strategy; timeout }) ->
Printf.printf "Wait strategy '%s' timed out after %.1fs\n" strategy timeout;
Lwt.fail_with "Container not ready"
| Error.Testcontainers_error (Error.Docker_connection_failed msg) ->
Printf.printf "Cannot connect to Docker: %s\n" msg;
Printf.printf "Is Docker running?\n";
Lwt.fail_with "Docker not available"
| exn ->
Lwt.fail exn)
Common Error Scenarios
Docker Not Running
(* Error: Docker_connection_failed "Connection refused" *)
(* Solution: Start Docker *)
(* docker info *)
Image Not Found
(* Error: Image_pull_failed { image = "invalid:tag"; message = "not found" } *)
(* Solution: Check image name/tag *)
let request = Container_request.create "postgres:16" (* valid tag *)
Port Not Exposed
(* Error: Port_not_mapped { container_port = 5432; protocol = "tcp" } *)
(* Solution: Expose the port *)
let request =
Container_request.create "postgres:16"
|> Container_request.with_exposed_port (Port.tcp 5432) (* add this *)
Wait Strategy Timeout
(* Error: Wait_timeout { strategy = "log:ready"; timeout = 60.0 } *)
(* Solutions: *)
(* 1. Increase timeout *)
Wait_strategy.with_timeout 120.0 strategy
(* 2. Use correct wait condition *)
Wait_strategy.for_log "database system is ready" (* exact message *)
(* 3. Check container logs for actual startup message *)
let* logs = Container.logs container in
print_endline logs
Container Start Failed
(* Error: Container_start_failed { id = "abc"; message = "port already allocated" } *)
(* Solutions: *)
(* 1. Remove conflicting container *)
(* docker ps -a *)
(* docker rm -f <container_id> *)
(* 2. Use random port mapping (default behavior) *)
Container_request.with_exposed_port (Port.tcp 5432) (* Docker assigns random host port *)
Ensuring Cleanup on Error
with_container ensures cleanup even on errors:
let test_that_fails () =
Container.with_container request (fun container ->
(* This error won't leak the container *)
failwith "Test failed"
)
(* Container is still cleaned up *)
For manual lifecycle management, use Lwt.finalize:
let test_with_manual_cleanup () =
let* container = Container.start request in
Lwt.finalize
(fun () ->
(* Test code that might fail *)
do_something container)
(fun () ->
(* Always runs, even on error *)
Container.terminate container)
Debugging Tips
Enable Verbose Logging
let debug_container container =
let* logs = Container.logs container in
Printf.printf "=== Container Logs ===\n%s\n" logs;
let* state = Container.state container in
Printf.printf "State: %s\n" (match state with
| `Running -> "running"
| `Exited -> "exited"
| _ -> "other");
Lwt.return_unit
Check Docker Directly
# List containers
docker ps -a
# Check specific container
docker logs <container_id>
docker inspect <container_id>
# Check Docker events
docker events --since 1h
Inspect Failed Container
Don't terminate failed containers immediately—inspect them first:
let debug_on_failure () =
let* container = Container.start request in
Lwt.catch
(fun () ->
run_tests container)
(fun exn ->
(* Debug before cleanup *)
let* logs = Container.logs container in
Printf.printf "Container logs:\n%s\n" logs;
let id = Container.id container in
Printf.printf "Container ID: %s\n" id;
Printf.printf "Inspect with: docker inspect %s\n" id;
(* Optionally, don't terminate to allow manual inspection *)
(* let* () = Container.terminate container in *)
Lwt.fail exn)
Testing Error Handling
let test_handles_docker_errors _switch () =
(* Test with invalid image *)
let request = Container_request.create "this-image-does-not-exist:never" in
Lwt.catch
(fun () ->
let* _container = Container.start request in
Alcotest.fail "Should have failed")
(function
| Error.Testcontainers_error (Error.Image_pull_failed _) ->
Lwt.return_unit (* Expected *)
| exn ->
Alcotest.fail (Printf.sprintf "Wrong error: %s" (Printexc.to_string exn)))