diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py
index 974985d8b4fc..b903c59a7192 100644
--- a/mypy/checkstrformat.py
+++ b/mypy/checkstrformat.py
@@ -199,15 +199,11 @@ def parse_format_value(
                     custom_match, start_pos=start_pos, non_standard_format_spec=True
                 )
             else:
-                msg.fail(
-                    "Invalid conversion specifier in format string",
-                    ctx,
-                    code=codes.STRING_FORMATTING,
-                )
+                msg.fail(message_registry.FORMAT_STR_INVALID_SPECIFIER, ctx)
                 return None
 
         if conv_spec.key and ("{" in conv_spec.key or "}" in conv_spec.key):
-            msg.fail("Conversion value must not contain { or }", ctx, code=codes.STRING_FORMATTING)
+            msg.fail(message_registry.FORMAT_STR_BRACES_IN_SPECIFIER, ctx)
             return None
         result.append(conv_spec)
 
@@ -218,11 +214,7 @@ def parse_format_value(
             and ("{" in conv_spec.format_spec or "}" in conv_spec.format_spec)
         ):
             if nested:
-                msg.fail(
-                    "Formatting nesting must be at most two levels deep",
-                    ctx,
-                    code=codes.STRING_FORMATTING,
-                )
+                msg.fail(message_registry.FORMAT_STR_NESTING_ATMOST_TWO_LEVELS, ctx)
                 return None
             sub_conv_specs = parse_format_value(conv_spec.format_spec, ctx, msg, nested=True)
             if sub_conv_specs is None:
@@ -260,11 +252,7 @@ def find_non_escaped_targets(
                 if pos < len(format_value) - 1 and format_value[pos + 1] == "}":
                     pos += 1
                 else:
-                    msg.fail(
-                        "Invalid conversion specifier in format string: unexpected }",
-                        ctx,
-                        code=codes.STRING_FORMATTING,
-                    )
+                    msg.fail(message_registry.FORMAT_STR_UNEXPECTED_RBRACE, ctx)
                     return None
         else:
             # Adjust nesting level, then either continue adding chars or move on.
@@ -279,11 +267,7 @@ def find_non_escaped_targets(
                 next_spec = ""
         pos += 1
     if nesting:
-        msg.fail(
-            "Invalid conversion specifier in format string: unmatched {",
-            ctx,
-            code=codes.STRING_FORMATTING,
-        )
+        msg.fail(message_registry.FORMAT_STR_UNMATCHED_LBRACE, ctx)
         return None
     return result
 
@@ -369,9 +353,8 @@ def check_specs_in_format_call(
                 ):
                     # TODO: add support for some custom specs like datetime?
                     self.msg.fail(
-                        "Unrecognized format" ' specification "{}"'.format(spec.format_spec[1:]),
+                        message_registry.UNRECOGNIZED_FORMAT_SPEC.format(spec.format_spec[1:]),
                         call,
-                        code=codes.STRING_FORMATTING,
                     )
                     continue
             # Adjust expected and actual types.
@@ -390,10 +373,10 @@ def check_specs_in_format_call(
                 # If the explicit conversion is given, then explicit conversion is called _first_.
                 if spec.conversion[1] not in "rsa":
                     self.msg.fail(
-                        'Invalid conversion type "{}",'
-                        ' must be one of "r", "s" or "a"'.format(spec.conversion[1]),
+                        message_registry.FORMAT_STR_INVALID_CONVERSION_TYPE.format(
+                            spec.conversion[1]
+                        ),
                         call,
-                        code=codes.STRING_FORMATTING,
                     )
                 actual_type = self.named_type("builtins.str")
 
@@ -433,13 +416,7 @@ def perform_special_format_checks(
             if has_type_component(actual_type, "builtins.bytes") and not custom_special_method(
                 actual_type, "__str__"
             ):
-                self.msg.fail(
-                    'If x = b\'abc\' then f"{x}" or "{}".format(x) produces "b\'abc\'", '
-                    'not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). '
-                    "Otherwise, decode the bytes",
-                    call,
-                    code=codes.STR_BYTES_PY3,
-                )
+                self.msg.fail(message_registry.FORMAT_STR_BYTES_USE_REPR, call)
         if spec.flags:
             numeric_types = UnionType(
                 [self.named_type("builtins.int"), self.named_type("builtins.float")]
@@ -451,11 +428,7 @@ def perform_special_format_checks(
                 and not is_subtype(actual_type, numeric_types)
                 and not custom_special_method(actual_type, "__format__")
             ):
-                self.msg.fail(
-                    "Numeric flags are only allowed for numeric types",
-                    call,
-                    code=codes.STRING_FORMATTING,
-                )
+                self.msg.fail(message_registry.FORMAT_STR_INVALID_NUMERIC_FLAG, call)
 
     def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Expression]:
         """Find replacement expression for every specifier in str.format() call.
@@ -469,19 +442,14 @@ def find_replacements_in_call(self, call: CallExpr, keys: list[str]) -> list[Exp
                 expr = self.get_expr_by_position(int(key), call)
                 if not expr:
                     self.msg.fail(
-                        "Cannot find replacement for positional"
-                        " format specifier {}".format(key),
-                        call,
-                        code=codes.STRING_FORMATTING,
+                        message_registry.FORMAT_STR_REPLACEMENT_NOT_FOUND.format(key), call
                     )
                     expr = TempNode(AnyType(TypeOfAny.from_error))
             else:
                 expr = self.get_expr_by_name(key, call)
                 if not expr:
                     self.msg.fail(
-                        "Cannot find replacement for named" ' format specifier "{}"'.format(key),
-                        call,
-                        code=codes.STRING_FORMATTING,
+                        message_registry.FORMAT_STR_NAMED_REPLACEMENT_NOT_FOUND.format(key), call
                     )
                     expr = TempNode(AnyType(TypeOfAny.from_error))
             result.append(expr)
@@ -555,11 +523,7 @@ def auto_generate_keys(self, all_specs: list[ConversionSpecifier], ctx: Context)
         some_defined = any(s.key and s.key.isdecimal() for s in all_specs)
         all_defined = all(bool(s.key) for s in all_specs)
         if some_defined and not all_defined:
-            self.msg.fail(
-                "Cannot combine automatic field numbering and manual field specification",
-                ctx,
-                code=codes.STRING_FORMATTING,
-            )
+            self.msg.fail(message_registry.FORMAT_STR_PARTIAL_FIELD_NUMBERING, ctx)
             return False
         if all_defined:
             return True
@@ -594,11 +558,7 @@ def apply_field_accessors(
             dummy, fnam="<format>", module=None, options=self.chk.options, errors=temp_errors
         )
         if temp_errors.is_errors():
-            self.msg.fail(
-                f'Syntax error in format specifier "{spec.field}"',
-                ctx,
-                code=codes.STRING_FORMATTING,
-            )
+            self.msg.fail(message_registry.FORMAT_STR_SYNTAX_ERROR.format(spec.field), ctx)
             return TempNode(AnyType(TypeOfAny.from_error))
 
         # These asserts are guaranteed by the original regexp.
@@ -637,10 +597,7 @@ class User(TypedDict):
         """
         if not isinstance(temp_ast, (MemberExpr, IndexExpr)):
             self.msg.fail(
-                "Only index and member expressions are allowed in"
-                ' format field accessors; got "{}"'.format(spec.field),
-                ctx,
-                code=codes.STRING_FORMATTING,
+                message_registry.FORMAT_STR_INVALID_ACCESSOR_EXPR.format(spec.field), ctx
             )
             return False
         if isinstance(temp_ast, MemberExpr):
@@ -651,10 +608,10 @@ class User(TypedDict):
                 assert spec.key, "Call this method only after auto-generating keys!"
                 assert spec.field
                 self.msg.fail(
-                    "Invalid index expression in format field"
-                    ' accessor "{}"'.format(spec.field[len(spec.key) :]),
+                    message_registry.FORMAT_STR_INVALID_INDEX_ACCESSOR.format(
+                        spec.field[len(spec.key) :]
+                    ),
                     ctx,
-                    code=codes.STRING_FORMATTING,
                 )
                 return False
             if isinstance(temp_ast.index, NameExpr):
@@ -683,11 +640,7 @@ def check_str_interpolation(self, expr: FormatStringExpr, replacements: Expressi
         specifiers = parse_conversion_specifiers(expr.value)
         has_mapping_keys = self.analyze_conversion_specifiers(specifiers, expr)
         if isinstance(expr, BytesExpr) and self.chk.options.python_version < (3, 5):
-            self.msg.fail(
-                "Bytes formatting is only supported in Python 3.5 and later",
-                replacements,
-                code=codes.STRING_FORMATTING,
-            )
+            self.msg.fail(message_registry.FORMAT_STR_BYTES_ABOVE_PY35, replacements)
             return AnyType(TypeOfAny.from_error)
 
         if has_mapping_keys is None:
@@ -794,9 +747,7 @@ def check_mapping_str_interpolation(
                     # Special case: for bytes formatting keys must be bytes.
                     if not isinstance(k, BytesExpr):
                         self.msg.fail(
-                            "Dictionary keys in bytes formatting must be bytes, not strings",
-                            expr,
-                            code=codes.STRING_FORMATTING,
+                            message_registry.FORMAT_STR_BYTES_DICT_KEYS_MUST_BE_BYTES, expr
                         )
                 key_str = cast(FormatStringExpr, k).value
                 mapping[key_str] = self.accept(v)
@@ -948,21 +899,12 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont
         if isinstance(expr, StrExpr):
             # Couple special cases for string formatting.
             if has_type_component(typ, "builtins.bytes"):
-                self.msg.fail(
-                    'If x = b\'abc\' then "%s" % x produces "b\'abc\'", not "abc". '
-                    'If this is desired behavior use "%r" % x. Otherwise, decode the bytes',
-                    context,
-                    code=codes.STR_BYTES_PY3,
-                )
+                self.msg.fail(message_registry.FORMAT_STR_BYTES_USE_REPR_OLD, context)
                 return False
         if isinstance(expr, BytesExpr):
             # A special case for bytes formatting: b'%s' actually requires bytes on Python 3.
             if has_type_component(typ, "builtins.str"):
-                self.msg.fail(
-                    "On Python 3 b'%s' requires bytes, not string",
-                    context,
-                    code=codes.STRING_FORMATTING,
-                )
+                self.msg.fail(message_registry.FORMAT_STR_BYTES_REQUIRED_PY3, context)
                 return False
         return True
 
@@ -1024,18 +966,10 @@ def conversion_type(
         INT_TYPES = REQUIRE_INT_NEW if format_call else REQUIRE_INT_OLD
         if p == "b" and not format_call:
             if self.chk.options.python_version < (3, 5):
-                self.msg.fail(
-                    'Format character "b" is only supported in Python 3.5 and later',
-                    context,
-                    code=codes.STRING_FORMATTING,
-                )
+                self.msg.fail(message_registry.FORMAT_STR_INVALID_BYTES_SPECIFIER_PY35, context)
                 return None
             if not isinstance(expr, BytesExpr):
-                self.msg.fail(
-                    'Format character "b" is only supported on bytes patterns',
-                    context,
-                    code=codes.STRING_FORMATTING,
-                )
+                self.msg.fail(message_registry.FORMAT_STR_INVALID_BYTES_SPECIFIER, context)
                 return None
             return self.named_type("builtins.bytes")
         elif p == "a":
diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py
index 50a82be9816d..947dbe9b81c5 100644
--- a/mypy/errorcodes.py
+++ b/mypy/errorcodes.py
@@ -112,10 +112,10 @@ def __str__(self) -> str:
 VALID_NEWTYPE: Final = ErrorCode(
     "valid-newtype", "Check that argument 2 to NewType is valid", "General"
 )
-STRING_FORMATTING: Final = ErrorCode(
+STRING_FORMATTING: Final[ErrorCode] = ErrorCode(
     "str-format", "Check that string formatting/interpolation is type-safe", "General"
 )
-STR_BYTES_PY3: Final = ErrorCode(
+STR_BYTES_PY3: Final[ErrorCode] = ErrorCode(
     "str-bytes-safe", "Warn about implicit coercions related to bytes and string types", "General"
 )
 EXIT_RETURN: Final = ErrorCode(
diff --git a/mypy/message_registry.py b/mypy/message_registry.py
index c5164d48fd13..1459e89d84fb 100644
--- a/mypy/message_registry.py
+++ b/mypy/message_registry.py
@@ -316,3 +316,74 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
 ARG_NAME_EXPECTED_STRING_LITERAL: Final = ErrorMessage(
     "Expected string literal for argument name, got {}", codes.SYNTAX
 )
+
+FORMAT_STR_INVALID_SPECIFIER: Final = ErrorMessage(
+    "Invalid conversion specifier in format string", codes.STRING_FORMATTING
+)
+FORMAT_STR_BRACES_IN_SPECIFIER: Final = ErrorMessage(
+    "Conversion value must not contain { or }", codes.STRING_FORMATTING
+)
+FORMAT_STR_NESTING_ATMOST_TWO_LEVELS: Final = ErrorMessage(
+    "Formatting nesting must be at most two levels deep", codes.STRING_FORMATTING
+)
+FORMAT_STR_UNEXPECTED_RBRACE: Final = ErrorMessage(
+    "Invalid conversion specifier in format string: unexpected }", codes.STRING_FORMATTING
+)
+FORMAT_STR_UNMATCHED_LBRACE: Final = ErrorMessage(
+    "Invalid conversion specifier in format string: unmatched {", codes.STRING_FORMATTING
+)
+UNRECOGNIZED_FORMAT_SPEC: Final = ErrorMessage(
+    'Unrecognized format specification "{}"', codes.STRING_FORMATTING
+)
+FORMAT_STR_INVALID_CONVERSION_TYPE: Final = ErrorMessage(
+    'Invalid conversion type "{}", must be one of "r", "s" or "a"', codes.STRING_FORMATTING
+)
+FORMAT_STR_BYTES_USE_REPR: Final = ErrorMessage(
+    'If x = b\'abc\' then f"{x}" or "{}".format(x) produces "b\'abc\'", '
+    'not "abc". If this is desired behavior, use f"{x!r}" or "{!r}".format(x). '
+    "Otherwise, decode the bytes",
+    codes.STR_BYTES_PY3,
+)
+FORMAT_STR_BYTES_USE_REPR_OLD: Final = ErrorMessage(
+    'If x = b\'abc\' then "%s" % x produces "b\'abc\'", not "abc". '
+    'If this is desired behavior use "%r" % x. Otherwise, decode the bytes',
+    codes.STR_BYTES_PY3,
+)
+FORMAT_STR_INVALID_NUMERIC_FLAG: Final = ErrorMessage(
+    "Numeric flags are only allowed for numeric types", codes.STRING_FORMATTING
+)
+FORMAT_STR_REPLACEMENT_NOT_FOUND: Final = ErrorMessage(
+    "Cannot find replacement for positional format specifier {}", codes.STRING_FORMATTING
+)
+FORMAT_STR_NAMED_REPLACEMENT_NOT_FOUND: Final = ErrorMessage(
+    'Cannot find replacement for named format specifier "{}"', codes.STRING_FORMATTING
+)
+FORMAT_STR_PARTIAL_FIELD_NUMBERING: Final = ErrorMessage(
+    "Cannot combine automatic field numbering and manual field specification",
+    codes.STRING_FORMATTING,
+)
+FORMAT_STR_SYNTAX_ERROR: Final = ErrorMessage(
+    'Syntax error in format specifier "{}"', codes.STRING_FORMATTING
+)
+FORMAT_STR_INVALID_ACCESSOR_EXPR: Final = ErrorMessage(
+    'Only index and member expressions are allowed in format field accessors; got "{}"',
+    codes.STRING_FORMATTING,
+)
+FORMAT_STR_INVALID_INDEX_ACCESSOR: Final = ErrorMessage(
+    'Invalid index expression in format field accessor "{}"', codes.STRING_FORMATTING
+)
+FORMAT_STR_BYTES_ABOVE_PY35: Final = ErrorMessage(
+    "Bytes formatting is only supported in Python 3.5 and later", codes.STRING_FORMATTING
+)
+FORMAT_STR_BYTES_DICT_KEYS_MUST_BE_BYTES: Final = ErrorMessage(
+    "Dictionary keys in bytes formatting must be bytes, not strings", codes.STRING_FORMATTING
+)
+FORMAT_STR_BYTES_REQUIRED_PY3: Final = ErrorMessage(
+    "On Python 3 b'%s' requires bytes, not string", codes.STRING_FORMATTING
+)
+FORMAT_STR_INVALID_BYTES_SPECIFIER_PY35: Final = ErrorMessage(
+    'Format character "b" is only supported in Python 3.5 and later', codes.STRING_FORMATTING
+)
+FORMAT_STR_INVALID_BYTES_SPECIFIER: Final = ErrorMessage(
+    'Format character "b" is only supported on bytes patterns', codes.STRING_FORMATTING
+)
diff --git a/mypy/messages.py b/mypy/messages.py
index 9d703a1a974a..1ad35ab2f47e 100644
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -16,7 +16,7 @@
 import re
 from contextlib import contextmanager
 from textwrap import dedent
-from typing import Any, Callable, Collection, Iterable, Iterator, List, Sequence, cast
+from typing import Any, Callable, Collection, Iterable, Iterator, List, Sequence, cast, overload
 from typing_extensions import Final
 
 import mypy.typeops
@@ -269,6 +269,7 @@ def span_from_context(ctx: Context) -> Iterable[int]:
             allow_dups=allow_dups,
         )
 
+    @overload
     def fail(
         self,
         msg: str,
@@ -278,10 +279,40 @@ def fail(
         file: str | None = None,
         allow_dups: bool = False,
         secondary_context: Context | None = None,
+    ) -> None:
+        ...
+
+    @overload
+    def fail(
+        self,
+        msg: message_registry.ErrorMessage,
+        context: Context | None,
+        *,
+        file: str | None = None,
+        allow_dups: bool = False,
+        secondary_context: Context | None = None,
+    ) -> None:
+        ...
+
+    def fail(
+        self,
+        msg: str | message_registry.ErrorMessage,
+        context: Context | None,
+        *,
+        code: ErrorCode | None = None,
+        file: str | None = None,
+        allow_dups: bool = False,
+        secondary_context: Context | None = None,
     ) -> None:
         """Report an error message (unless disabled)."""
+        if isinstance(msg, message_registry.ErrorMessage):
+            msg_str = msg.value
+            code = msg.code
+        else:
+            msg_str = msg
+
         self.report(
-            msg,
+            msg_str,
             context,
             "error",
             code=code,