Introduction
In Jane Street's Async library, a Deferred.t represents a value that will become available later. The function return has type 'a -> 'a Deferred.t, which means it takes an ordinary value and wraps it as an already-determined deferred. That sounds small, but understanding this one function is central to composing synchronous and asynchronous code correctly.
What return Actually Does
return does not start background work. It does not schedule I/O. It simply lifts a plain value into the deferred world so that a function can keep returning Deferred.t consistently.
1open Core
2open Async
3
4let answer : int Deferred.t =
5 return 42
6
7let () =
8 don't_wait_for (
9 let%map v = answer in
10 printf "answer=%d\n" v
11 );
12 never_returns (Scheduler.go ())
Because the deferred is already determined, there is no external wait involved. The point is interface consistency, not delayed execution.
According to the Async Deferred API documentation, return has the signature val return : 'a -> 'a t. That is the core idea to keep in mind while reading Async code.
Use return to Keep Async Signatures Consistent
A common reason to use return is that a function must return Deferred.t, but one branch already has the result immediately.
1open Core
2open Async
3
4let lookup cache key : string Deferred.t =
5 match Hashtbl.find cache key with
6| Some value -> return value | None -> let%bind () = after (Time_ns.Span.of_ms 10.) in return "loaded-from-db" ``` Without `return`, the two branches would have incompatible types. One branch would produce a plain `string`, while the other would produce `string Deferred.t`. This is one of the most practical uses of `return`: it lets synchronous and asynchronous paths share one API shape. ## `return` Versus `map` and `bind` `return` wraps a plain value. `map` transforms the result inside an existing deferred. `bind` sequences another asynchronous step that also returns a deferred. ```ocaml open Core open Async let get_user_id () : int Deferred.t = return 7 let fetch_profile id : string Deferred.t = let%bind () = after (Time_ns.Span.of_ms 5.) in return (sprintf "profile-%d" id) let pipeline () : string Deferred.t = let%bind user_id = get_user_id () in let%map profile = fetch_profile user_id in String.uppercase profile ``` A useful way to think about it is: - use `return` when you already have a plain value - use `map` when you have a deferred and want a pure transformation - use `bind` when the next step also returns a deferred If you use `map` where `bind` is required, you end up with nested deferred values, which is usually a sign that the composition is wrong. ## `return` Does Not Fix Blocking Code Developers sometimes wrap a blocking computation in `return` and assume that makes it asynchronous. It does not. ```ocaml let bad () : int Deferred.t = let result = expensive_blocking_call () in return result ``` If `expensive_blocking_call` blocks, the blocking already happened before `return` was reached. `return` only wraps the finished result. That distinction matters because Async is built around non-blocking composition. `return` is a typing and composition tool, not a concurrency primitive. ## `return` in Error-Aware Pipelines Async code often uses `Deferred.Or_error.t` or `Monitor.try_with`. `return` still matters there because successful values often need to be lifted into the async pipeline cleanly. ```ocaml open Core open Async let safe_job () : (string, exn) Result.t Deferred.t = Monitor.try_with (fun () -> let%bind () = after (Time_ns.Span.of_ms 1.) in return "ok") ``` The body still returns a deferred result, even though the successful final value is just a plain string. ## Common Pitfalls The biggest mistake is thinking `return` launches asynchronous work. It does not. It only wraps a value that already exists. Another problem is returning a plain value in one branch and a deferred value in another. Async functions should stay consistent about their return type. Developers also sometimes confuse `map` and `bind`. If the next step returns `Deferred.t`, use `bind`, not `map`. Finally, do not use `return` as a way to hide blocking code. If a function blocks before `return`, it is still blocking Async's execution flow. ## Summary - '`return` wraps a plain value as an already-determined `Deferred.t`.' - Use it when a function must return `Deferred.t` but the result is already available. - Use `map` for pure transformations and `bind` for additional asynchronous steps. - '`return` does not create concurrency or fix blocking code.' - Treat `return` as a composition tool that keeps Async interfaces consistent.